From ee95c5fab7c563f55cf92719097fd5ffa91f670f Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 4 Jun 2026 13:55:41 +0200 Subject: [PATCH 01/17] Fixes the symbol rendering --- bittensor_cli/src/commands/subnets/subnets.py | 47 ++++++++++--------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 65404e6da..5d813a6bc 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -1460,6 +1460,11 @@ async def show_subnet( ), ) + # Append a LEFT-TO-RIGHT MARK to the symbol so RTL symbols + # don't get BiDi-reordered by terminals like + # Terminal.app, which misaligns the table columns. + symbol = f"{subnet_info.symbol}‎" + rows = [] json_out_rows = [] for idx in sorted_indices: @@ -1500,25 +1505,25 @@ async def show_subnet( conv_cell = _format_conviction_cell( conviction=conv_value, netuid=netuid_, - symbol=subnet_info.symbol, + symbol=symbol, verbose=verbose, ) rows.append( ( str(idx), # UID - f"{metagraph_info.total_stake[idx].tao:.4f} {subnet_info.symbol}" + f"{metagraph_info.total_stake[idx].tao:.4f} {symbol}" if verbose - else f"{millify_tao(metagraph_info.total_stake[idx])} {subnet_info.symbol}", # Stake - f"{metagraph_info.alpha_stake[idx].tao:.4f} {subnet_info.symbol}" + else f"{millify_tao(metagraph_info.total_stake[idx])} {symbol}", # Stake + f"{metagraph_info.alpha_stake[idx].tao:.4f} {symbol}" if verbose - else f"{millify_tao(metagraph_info.alpha_stake[idx])} {subnet_info.symbol}", # Alpha Stake + else f"{millify_tao(metagraph_info.alpha_stake[idx])} {symbol}", # Alpha Stake f"τ {tao_stake.tao:.4f}" if verbose else f"τ {millify_tao(tao_stake)}", # Tao Stake f"{metagraph_info.dividends[idx]:.6f}", # Dividends f"{metagraph_info.incentives[idx]:.6f}", # Incentive - f"{Balance.from_tao(metagraph_info.emission[idx].tao).set_unit(netuid_).tao:.6f} {subnet_info.symbol}", # Emissions + f"{Balance.from_tao(metagraph_info.emission[idx].tao).set_unit(netuid_).tao:.6f} {symbol}", # Emissions conv_cell, # Conv. (α-eq) f"{metagraph_info.hotkeys[idx][:6]}" if not verbose @@ -1565,31 +1570,31 @@ async def show_subnet( # Add columns to the table table.add_column("UID", style="grey89", no_wrap=True, justify="center") table.add_column( - f"Stake ({subnet_info.symbol})", + f"Stake ({symbol})", style=COLOR_PALETTE["POOLS"]["ALPHA_IN"], no_wrap=True, justify="right", - footer=f"{stake_sum:.4f} {subnet_info.symbol}" + footer=f"{stake_sum:.4f} {symbol}" if verbose - else f"{millify_tao(stake_sum)} {subnet_info.symbol}", + else f"{millify_tao(stake_sum)} {symbol}", ) table.add_column( - f"Alpha ({subnet_info.symbol})", + f"Alpha ({symbol})", style=COLOR_PALETTE["POOLS"]["EXTRA_2"], no_wrap=True, justify="right", - footer=f"{alpha_sum:.4f} {subnet_info.symbol}" + footer=f"{alpha_sum:.4f} {symbol}" if verbose - else f"{millify_tao(alpha_sum)} {subnet_info.symbol}", + else f"{millify_tao(alpha_sum)} {symbol}", ) table.add_column( "Tao (τ)", style=COLOR_PALETTE["POOLS"]["EXTRA_2"], no_wrap=True, justify="right", - footer=f"{tao_sum:.4f} {subnet_info.symbol}" + footer=f"{tao_sum:.4f} {symbol}" if verbose - else f"{millify_tao(tao_sum)} {subnet_info.symbol}", + else f"{millify_tao(tao_sum)} {symbol}", ) table.add_column( "Dividends", @@ -1600,14 +1605,14 @@ async def show_subnet( ) table.add_column("Incentive", style="#5fd7ff", no_wrap=True, justify="center") table.add_column( - f"Emissions ({subnet_info.symbol})", + f"Emissions ({symbol})", style=COLOR_PALETTE["POOLS"]["EMISSION"], no_wrap=True, justify="center", - footer=f"{emission_sum:.4f} {subnet_info.symbol}", + footer=f"{emission_sum:.4f} {symbol}", ) table.add_column( - f"Conviction ({subnet_info.symbol}-eq)", + f"Conviction ({symbol}-eq)", style=COLOR_PALETTE["POOLS"]["EXTRA_2"], no_wrap=True, justify="center", @@ -1668,9 +1673,9 @@ async def show_subnet( total_mechanisms = mechanism_count if mechanism_count is not None else 1 _total_locked = Balance.from_rao(total_locked_rao).set_unit(netuid_) total_locked = ( - f"{millify_tao(_total_locked.tao)} {subnet_info.symbol}" + f"{millify_tao(_total_locked.tao)} {symbol}" if not verbose - else f"{_total_locked.tao:.4f} {subnet_info.symbol}" + else f"{_total_locked.tao:.4f} {symbol}" ) output_dict = { @@ -1714,11 +1719,11 @@ async def show_subnet( f"{mech_line}" f"{total_mech_line}" f"\n Owner: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{subnet_info.owner_coldkey}{' (' + owner_identity + ')' if owner_identity else ''}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]" - f"\n Rate: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{subnet_info.price.tao:.4f} τ/{subnet_info.symbol}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" + f"\n Rate: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{subnet_info.price.tao:.4f} τ/{symbol}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" f"\n EMA TAO Inflow: [{COLOR_PALETTE['STAKE']['TAO']}]τ {ema_tao_inflow.tao:.4f}[/{COLOR_PALETTE['STAKE']['TAO']}]" f"\n Emission: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]τ {subnet_info.tao_in_emission.tao:,.4f}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" f"\n TAO Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]τ {tao_pool}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" - f"\n Alpha Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{alpha_pool} {subnet_info.symbol}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" + f"\n Alpha Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{alpha_pool} {symbol}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" f"\n Total locked: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}] {total_locked} [/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" # f"\n Stake: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{subnet_info.alpha_out.tao:,.5f} {subnet_info.symbol}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" f"\n Tempo: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{subnet_info.blocks_since_last_step}/{subnet_info.tempo}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" From ec10bb07b95d07bc9148e2f46300688945a2d29e Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 4 Jun 2026 14:21:11 +0200 Subject: [PATCH 02/17] Improves readability by using the dot notation of COLOR_PALETTE rather than the dict[] --- bittensor_cli/src/commands/subnets/subnets.py | 198 +++++++++--------- 1 file changed, 97 insertions(+), 101 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 5d813a6bc..a69d3aea9 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -14,7 +14,7 @@ from rich.table import Column, Table from rich import box -from bittensor_cli.src import COLOR_PALETTE +from bittensor_cli.src import COLOR_PALETTE as COLOR from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.locks import ( hotkey_aggregate_conviction, @@ -265,19 +265,17 @@ async def _find_event_attributes_in_extrinsic_receipt( sn_burn_cost = await burn_cost(subtensor) if sn_burn_cost > your_balance: print_error( - f"Your balance of: [{COLOR_PALETTE.POOLS.TAO}]{your_balance}[{COLOR_PALETTE.POOLS.TAO}]" + f"Your balance of: [{COLOR.POOLS.TAO}]{your_balance}[{COLOR.POOLS.TAO}]" f" is not enough to burn " - f"[{COLOR_PALETTE.POOLS.TAO}]{sn_burn_cost}[{COLOR_PALETTE.POOLS.TAO}] " + f"[{COLOR.POOLS.TAO}]{sn_burn_cost}[{COLOR.POOLS.TAO}] " f"to register a subnet." ) return False, None, None if prompt: - console.print( - f"Your balance is: [{COLOR_PALETTE['POOLS']['TAO']}]{your_balance}" - ) + console.print(f"Your balance is: [{COLOR.P.TAO}]{your_balance}") if not confirm_action( - f"Do you want to burn [{COLOR_PALETTE['POOLS']['TAO']}]{sn_burn_cost} to register a subnet?", + f"Do you want to burn [{COLOR.P.TAO}]{sn_burn_cost}[/{COLOR.P.TAO}] to register a subnet?", decline=decline, quiet=quiet, ): @@ -456,8 +454,8 @@ def define_table( total_tao_flow_ema: float, ): defined_table = create_table( - title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Subnets" - f"\nNetwork: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{subtensor.network}\n\n", + title=f"\n[{COLOR.G.HEADER}]Subnets[/{COLOR.G.HEADER}]" + f"\nNetwork: [{COLOR.G.SUBHEADING}]{subtensor.network}\n\n", ) defined_table.add_column( @@ -480,42 +478,42 @@ def define_table( ) defined_table.add_column( f"[bold white]Emission ({Balance.get_unit(0)})", - style=COLOR_PALETTE["POOLS"]["EMISSION"], + style=COLOR.P.EMISSION, justify="left", footer=f"τ {total_emissions}", ) defined_table.add_column( f"[bold white]Net Inflow EMA ({Balance.get_unit(0)})", - style=COLOR_PALETTE["POOLS"]["ALPHA_OUT"], + style=COLOR.P.ALPHA_OUT, justify="left", footer=f"τ {total_tao_flow_ema}", ) defined_table.add_column( f"[bold white]P ({Balance.get_unit(0)}_in, {Balance.get_unit(1)}_in)", - style=COLOR_PALETTE["STAKE"]["TAO"], + style=COLOR.S.TAO, justify="left", footer=f"{tao_emission_percentage}", ) defined_table.add_column( f"[bold white]Stake ({Balance.get_unit(1)}_out)", - style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], + style=COLOR.STAKE_ALPHA, justify="left", ) defined_table.add_column( f"[bold white]Supply ({Balance.get_unit(1)})", - style=COLOR_PALETTE["POOLS"]["ALPHA_IN"], + style=COLOR.P.ALPHA_IN, justify="left", ) defined_table.add_column( "[bold white]Tempo (k/n)", - style=COLOR_PALETTE["GENERAL"]["TEMPO"], + style=COLOR.G.TEMPO, justify="left", overflow="fold", ) defined_table.add_column( "[bold white]Mechanisms", - style=COLOR_PALETTE["GENERAL"]["SUBHEADING_EXTRA_1"], + style=COLOR.G.SUBHEAD_EX_1, justify="center", ) return defined_table @@ -583,7 +581,7 @@ def _create_table(subnets_, block_number_, mechanisms, ema_tao_inflow): # Prepare cells netuid_cell = str(netuid) subnet_name_cell = ( - f"[{COLOR_PALETTE.G.SYM}]{subnet.symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE.G.SYM}]" + f"[{COLOR.G.SYM}]{subnet.symbol if netuid != 0 else 'τ'}[/{COLOR.G.SYM}]" f" {get_subnet_name(subnet)}" ) emission_cell = f"τ {emission_tao:,.4f}" @@ -857,7 +855,7 @@ def format_liquidity_cell( netuid_cell = str(netuid) subnet_name_cell = ( - f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{subnet.symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" + f"[{COLOR.G.SYM}]{subnet.symbol if netuid != 0 else 'τ'}[/{COLOR.G.SYM}]" f" {get_subnet_name(subnet)}" ) emission_cell = format_cell( @@ -947,7 +945,7 @@ def format_liquidity_cell( else: block_change_text = "" tempo_cell = ( - (f"{subnet.blocks_since_last_step}/{subnet.tempo}{block_change_text}") + f"{subnet.blocks_since_last_step}/{subnet.tempo}{block_change_text}" if netuid != 0 else "-/-" ) @@ -1083,9 +1081,7 @@ def format_liquidity_cell( ).lower() if display_table == "q": - console.print( - f"[{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_1']}]Column descriptions skipped." - ) + console.print(f"[{COLOR.G.SUBHEAD_EX_1}]Column descriptions skipped.") else: header = """ [bold white]Description[/bold white]: The table displays information about each subnet. The columns are as follows: @@ -1177,41 +1173,41 @@ async def show_root(): tao_sum = sum(root_state.tao_stake).tao table = create_table( - title=f"[{COLOR_PALETTE.G.HEADER}]Root Network\n[{COLOR_PALETTE.G.SUBHEAD}]" - f"Network: {subtensor.network}[/{COLOR_PALETTE.G.SUBHEAD}]\n", + title=f"[{COLOR.G.HEADER}]Root Network\n[{COLOR.G.SUBHEAD}]" + f"Network: {subtensor.network}[/{COLOR.G.SUBHEAD}]\n", ) table.add_column("[bold white]Position", style="white", justify="center") table.add_column( "Tao (τ)", - style=COLOR_PALETTE["POOLS"]["EXTRA_2"], + style=COLOR.P.EXTRA_2, no_wrap=True, justify="right", footer=f"{tao_sum:.4f} τ" if verbose else f"{millify_tao(tao_sum)} τ", ) table.add_column( f"[bold white]Emission ({Balance.get_unit(0)}/block)", - style=COLOR_PALETTE["POOLS"]["EMISSION"], + style=COLOR.P.EMISSION, justify="center", ) table.add_column( "[bold white]Hotkey", - style=COLOR_PALETTE["GENERAL"]["HOTKEY"], + style=COLOR.G.HK, justify="center", ) table.add_column( "[bold white]Coldkey", - style=COLOR_PALETTE["GENERAL"]["COLDKEY"], + style=COLOR.G.CK, justify="center", ) table.add_column( "[bold white]Identity", - style=COLOR_PALETTE["GENERAL"]["SYMBOL"], + style=COLOR.G.SYM, justify="left", ) table.add_column( "[bold white]Claim Type", - style=COLOR_PALETTE["GENERAL"]["SUBHEADING"], + style=COLOR.G.SUBHEADING, justify="center", ) @@ -1298,12 +1294,12 @@ async def show_root(): else f"{root_info.price.tao:,.4f}" ) console.print( - f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Root Network (Subnet 0)[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" - f"\n Rate: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{rate} τ/τ[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" - f"\n Emission: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]τ 0[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" - f"\n TAO Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]τ {tao_pool}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" - f"\n Stake: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]τ {stake}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" - f"\n Tempo: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{root_info.blocks_since_last_step}/{root_info.tempo}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" + f"[{COLOR.G.SUBHEADING}]Root Network (Subnet 0)[/{COLOR.G.SUBHEADING}]" + f"\n Rate: [{COLOR.G.HK}]{rate} τ/τ[/{COLOR.G.HK}]" + f"\n Emission: [{COLOR.G.HK}]τ 0[/{COLOR.G.HK}]" + f"\n TAO Pool: [{COLOR.P.ALPHA_IN}]τ {tao_pool}[/{COLOR.P.ALPHA_IN}]" + f"\n Stake: [{COLOR.S.ALPHA}]τ {stake}[/{COLOR.S.ALPHA}]" + f"\n Tempo: [{COLOR.S.ALPHA}]{root_info.blocks_since_last_step}/{root_info.tempo}[/{COLOR.S.ALPHA}]" ) console.print( """ @@ -1346,7 +1342,7 @@ async def show_root(): identity_str = f" ({validator_identity})" if validator_identity else "" console.print( - f"\nSelected delegate: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{selected_hotkey}{identity_str}" + f"\nSelected delegate: [{COLOR.G.SUBHEADING}]{selected_hotkey}{identity_str}" ) return selected_hotkey @@ -1412,9 +1408,9 @@ async def show_subnet( mechanism_label = f"Mechanism {selected_mechanism_id}" table = create_table( - title=f"[{COLOR_PALETTE['GENERAL']['HEADER']}]Subnet [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_}" + title=f"[{COLOR.G.HEADER}]Subnet [{COLOR.G.SUBHEADING}]{netuid_}" f"{': ' + get_subnet_name(subnet_info)}" - f"\n[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Network: {subtensor.network} • {mechanism_label}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\n", + f"\n[{COLOR.G.SUBHEADING}]Network: {subtensor.network} • {mechanism_label}[/{COLOR.G.SUBHEADING}]\n", ) # For table footers @@ -1571,7 +1567,7 @@ async def show_subnet( table.add_column("UID", style="grey89", no_wrap=True, justify="center") table.add_column( f"Stake ({symbol})", - style=COLOR_PALETTE["POOLS"]["ALPHA_IN"], + style=COLOR.P.ALPHA_IN, no_wrap=True, justify="right", footer=f"{stake_sum:.4f} {symbol}" @@ -1580,7 +1576,7 @@ async def show_subnet( ) table.add_column( f"Alpha ({symbol})", - style=COLOR_PALETTE["POOLS"]["EXTRA_2"], + style=COLOR.P.EXTRA_2, no_wrap=True, justify="right", footer=f"{alpha_sum:.4f} {symbol}" @@ -1589,7 +1585,7 @@ async def show_subnet( ) table.add_column( "Tao (τ)", - style=COLOR_PALETTE["POOLS"]["EXTRA_2"], + style=COLOR.P.EXTRA_2, no_wrap=True, justify="right", footer=f"{tao_sum:.4f} {symbol}" @@ -1598,7 +1594,7 @@ async def show_subnet( ) table.add_column( "Dividends", - style=COLOR_PALETTE["POOLS"]["EMISSION"], + style=COLOR.P.EMISSION, no_wrap=True, justify="center", footer=f"{dividends_sum:.3f}", @@ -1606,38 +1602,38 @@ async def show_subnet( table.add_column("Incentive", style="#5fd7ff", no_wrap=True, justify="center") table.add_column( f"Emissions ({symbol})", - style=COLOR_PALETTE["POOLS"]["EMISSION"], + style=COLOR.P.EMISSION, no_wrap=True, justify="center", footer=f"{emission_sum:.4f} {symbol}", ) table.add_column( f"Conviction ({symbol}-eq)", - style=COLOR_PALETTE["POOLS"]["EXTRA_2"], + style=COLOR.P.EXTRA_2, no_wrap=True, justify="center", ) table.add_column( "Hotkey", - style=COLOR_PALETTE["GENERAL"]["HOTKEY"], + style=COLOR.G.HK, no_wrap=True, justify="center", ) table.add_column( "Coldkey", - style=COLOR_PALETTE["GENERAL"]["COLDKEY"], + style=COLOR.G.CK, no_wrap=True, justify="center", ) table.add_column( "Identity", - style=COLOR_PALETTE["GENERAL"]["SYMBOL"], + style=COLOR.G.SYM, no_wrap=True, justify="left", ) table.add_column( "Claim Type", - style=COLOR_PALETTE["GENERAL"]["SUBHEADING"], + style=COLOR.G.SUBHEADING, no_wrap=True, justify="center", ) @@ -1704,30 +1700,30 @@ async def show_subnet( json_console.print(json.dumps(output_dict)) mech_line = ( - f"\n Mechanism ID: [{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_1']}]#{selected_mechanism_id}" - f"[/{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_1']}]" + f"\n Mechanism ID: [{COLOR.G.SUBHEAD_EX_1}]#{selected_mechanism_id}" + f"[/{COLOR.G.SUBHEAD_EX_1}]" if total_mechanisms > 1 else "" ) total_mech_line = ( - f"\n Total mechanisms: [{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_2']}]" - f"{total_mechanisms}[/{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_2']}]" + f"\n Total mechanisms: [{COLOR.G.SUBHEAD_EX_2}]" + f"{total_mechanisms}[/{COLOR.G.SUBHEAD_EX_2}]" ) console.print( - f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Subnet {netuid_}{subnet_name_display}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f"[{COLOR.G.SUBHEADING}]Subnet {netuid_}{subnet_name_display}[/{COLOR.G.SUBHEADING}]" f"{mech_line}" f"{total_mech_line}" - f"\n Owner: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{subnet_info.owner_coldkey}{' (' + owner_identity + ')' if owner_identity else ''}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]" - f"\n Rate: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{subnet_info.price.tao:.4f} τ/{symbol}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" - f"\n EMA TAO Inflow: [{COLOR_PALETTE['STAKE']['TAO']}]τ {ema_tao_inflow.tao:.4f}[/{COLOR_PALETTE['STAKE']['TAO']}]" - f"\n Emission: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]τ {subnet_info.tao_in_emission.tao:,.4f}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" - f"\n TAO Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]τ {tao_pool}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" - f"\n Alpha Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{alpha_pool} {symbol}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" - f"\n Total locked: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}] {total_locked} [/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" - # f"\n Stake: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{subnet_info.alpha_out.tao:,.5f} {subnet_info.symbol}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" - f"\n Tempo: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{subnet_info.blocks_since_last_step}/{subnet_info.tempo}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" - f"\n Registration cost (recycled): [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]τ {current_registration_burn.tao:.4f}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" + f"\n Owner: [{COLOR.G.CK}]{subnet_info.owner_coldkey}{' (' + owner_identity + ')' if owner_identity else ''}[/{COLOR.G.CK}]" + f"\n Rate: [{COLOR.G.HK}]{subnet_info.price.tao:.4f} τ/{symbol}[/{COLOR.G.HK}]" + f"\n EMA TAO Inflow: [{COLOR.S.TAO}]τ {ema_tao_inflow.tao:.4f}[/{COLOR.S.TAO}]" + f"\n Emission: [{COLOR.G.HK}]τ {subnet_info.tao_in_emission.tao:,.4f}[/{COLOR.G.HK}]" + f"\n TAO Pool: [{COLOR.P.ALPHA_IN}]τ {tao_pool}[/{COLOR.P.ALPHA_IN}]" + f"\n Alpha Pool: [{COLOR.P.ALPHA_IN}]{alpha_pool} {symbol}[/{COLOR.P.ALPHA_IN}]" + f"\n Total locked: [{COLOR.P.ALPHA_IN}] {total_locked} [/{COLOR.P.ALPHA_IN}]" + # f"\n Stake: [{COLOR.S.ALPHA}]{subnet_info.alpha_out.tao:,.5f} {subnet_info.symbol}[/{COLOR.S.ALPHA}]" + f"\n Tempo: [{COLOR.S.ALPHA}]{subnet_info.blocks_since_last_step}/{subnet_info.tempo}[/{COLOR.S.ALPHA}]" + f"\n Registration cost (recycled): [{COLOR.S.ALPHA}]τ {current_registration_burn.tao:.4f}[/{COLOR.S.ALPHA}]" ) # console.print( # """ @@ -1769,7 +1765,7 @@ async def show_subnet( identity = "" if row_data[9] == "~" else row_data[9] identity_str = f" ({identity})" if identity else "" console.print( - f"\nSelected delegate: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{hotkey}{identity_str}" + f"\nSelected delegate: [{COLOR.G.SUBHEADING}]{hotkey}{identity_str}" ) return hotkey else: @@ -1805,7 +1801,7 @@ async def burn_cost( ) else: console.print( - f"Subnet burn cost: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_burn_cost}" + f"Subnet burn cost: [{COLOR.S.AMOUNT}]{current_burn_cost}" ) return current_burn_cost else: @@ -1975,9 +1971,9 @@ async def _storage_key(storage_fn: str) -> StorageKey: # Show creation table. table = create_table( title=( - f"\n[{COLOR_PALETTE.G.HEADER}]" - f"Register to [{COLOR_PALETTE.G.SUBHEAD}]netuid: {netuid}[/{COLOR_PALETTE.G.SUBHEAD}]" - f"\nNetwork: [{COLOR_PALETTE.G.SUBHEAD}]{subtensor.network}[/{COLOR_PALETTE.G.SUBHEAD}]\n" + f"\n[{COLOR.G.HEADER}]" + f"Register to [{COLOR.G.SUBHEAD}]netuid: {netuid}[/{COLOR.G.SUBHEAD}]" + f"\nNetwork: [{COLOR.G.SUBHEAD}]{subtensor.network}[/{COLOR.G.SUBHEAD}]\n" ), ) table.add_column( @@ -1985,32 +1981,32 @@ async def _storage_key(storage_fn: str) -> StorageKey: ) table.add_column( "Symbol", - style=COLOR_PALETTE["GENERAL"]["SYMBOL"], + style=COLOR.G.SYM, no_wrap=True, justify="center", ) table.add_column( f"Cost ({Balance.get_unit(0)})", - style=COLOR_PALETTE["POOLS"]["TAO"], + style=COLOR["POOLS"]["TAO"], no_wrap=True, justify="center", ) if with_limit is not None and limit is not None: table.add_column( f"Limit Cost (+{limit * 100:g}%)", - style=COLOR_PALETTE["POOLS"]["TAO"], + style=COLOR["POOLS"]["TAO"], no_wrap=True, justify="center", ) table.add_column( "Hotkey", - style=COLOR_PALETTE["GENERAL"]["HOTKEY"], + style=COLOR.G.HK, no_wrap=True, justify="center", ) table.add_column( "Coldkey", - style=COLOR_PALETTE["GENERAL"]["COLDKEY"], + style=COLOR.G.CK, no_wrap=True, justify="center", ) @@ -2031,9 +2027,9 @@ async def _storage_key(storage_fn: str) -> StorageKey: console.print(table) if not ( confirm_action( - f"Your balance is: [{COLOR_PALETTE.G.BAL}]{balance}[/{COLOR_PALETTE.G.BAL}]\n" + f"Your balance is: [{COLOR.G.BAL}]{balance}[/{COLOR.G.BAL}]\n" f"The cost to register by recycle is " - f"[{COLOR_PALETTE.G.COST}]{current_recycle}[/{COLOR_PALETTE.G.COST}].\n" + f"[{COLOR.G.COST}]{current_recycle}[/{COLOR.G.COST}].\n" f"Do you want to continue?", default=False, decline=decline, @@ -2602,11 +2598,11 @@ def create_identity_table(title: str = None): Column( "Item", justify="right", - style=COLOR_PALETTE["GENERAL"]["SUBHEADING_MAIN"], + style=COLOR["GENERAL"]["SUBHEADING_MAIN"], no_wrap=True, ), - Column("Value", style=COLOR_PALETTE["GENERAL"]["SUBHEADING"]), - title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]{title}\n", + Column("Value", style=COLOR.G.SUBHEADING), + title=f"\n[{COLOR.G.HEADER}]{title}\n", ) return table @@ -3123,38 +3119,38 @@ async def subnet_conviction( "—" if king is None else king if verbose else f"{king[:6]}...{king[-6:]}" ) table = create_table( - title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Subnet Conviction" - f"\nNetwork: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{subtensor.network}" + title=f"\n[{COLOR.G.HEADER}]Subnet Conviction" + f"\nNetwork: [{COLOR.G.SUBHEADING}]{subtensor.network}" f" • Block: {current_block:,}" - f"[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\n\n", + f"[/{COLOR.G.SUBHEADING}]\n\n", show_footer=False, ) table.add_column("[bold white]Rank", style="grey89", justify="center") table.add_column( "[bold white]Conviction", - style=COLOR_PALETTE["POOLS"]["EXTRA_2"], + style=COLOR.P.EXTRA_2, justify="center", ) table.add_column("[bold white]Share", justify="center") table.add_column( f"[bold white]Locked ({subnet_info.symbol})\n[white]Perpetual | Decay[/white]", - style=COLOR_PALETTE["POOLS"]["ALPHA_IN"], + style=COLOR.P.ALPHA_IN, justify="center", no_wrap=True, ) table.add_column( "[bold white]Hotkey", - style=COLOR_PALETTE["GENERAL"]["HOTKEY"], + style=COLOR.G.HK, justify="center", ) table.add_column( "[bold white]Coldkey", - style=COLOR_PALETTE["GENERAL"]["COLDKEY"], + style=COLOR.G.CK, justify="center", ) table.add_column( "[bold white]Identity", - style=COLOR_PALETTE["GENERAL"]["SUBHEADING"], + style=COLOR.G.SUBHEADING, justify="center", ) table.add_column("[bold white]Role", justify="center") @@ -3209,9 +3205,9 @@ async def subnet_conviction( identity_str = f" ({identity})" if identity else "" selected_hotkey = selected_record["hotkey"] console.print( - f"\nSelected hotkey: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f"\nSelected hotkey: [{COLOR.G.SUBHEADING}]" f"{selected_hotkey}{identity_str}" - f"[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f"[/{COLOR.G.SUBHEADING}]" ) return selected_hotkey @@ -3220,20 +3216,20 @@ async def subnet_conviction( console.print() console.print( - f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Subnet {netuid}: " - f"{get_subnet_name(subnet_info)}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" - f"\n Total conviction: [{COLOR_PALETTE['POOLS']['EXTRA_2']}]" + f"[{COLOR.G.SUBHEADING}]Subnet {netuid}: " + f"{get_subnet_name(subnet_info)}[/{COLOR.G.SUBHEADING}]" + f"\n Total conviction: [{COLOR.P.EXTRA_2}]" f"{_format_conviction_cell(total_conviction, netuid, symbol, verbose)}" - f"[/{COLOR_PALETTE['POOLS']['EXTRA_2']}]" - f"\n 10% threshold: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" + f"[/{COLOR.P.EXTRA_2}]" + f"\n 10% threshold: [{COLOR.P.ALPHA_IN}]" f"{_format_conviction_cell(Decimal(threshold_alpha_eq), netuid, symbol, verbose)}" - f"[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" - f"\n Threshold used: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]" - f"{pct_of_threshold:.0f}%[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" - f"\n Subnet age: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" - f"{blocks_to_duration(age_blocks)}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" - f"\n Top hotkey: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]" - f"{king_cell}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" + f"[/{COLOR.P.ALPHA_IN}]" + f"\n Threshold used: [{COLOR.G.HK}]" + f"{pct_of_threshold:.0f}%[/{COLOR.G.HK}]" + f"\n Subnet age: [{COLOR.S.ALPHA}]" + f"{blocks_to_duration(age_blocks)}[/{COLOR.S.ALPHA}]" + f"\n Top hotkey: [{COLOR.G.HK}]" + f"{king_cell}[/{COLOR.G.HK}]" ) unlock_half_life = blocks_to_duration(int(unlock_rate * math.log(2))) maturity_half_life = blocks_to_duration(int(maturity_rate * math.log(2))) From 4059dd7787985cce6ea7e5409bd0c4923bf50bf1 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 4 Jun 2026 14:25:16 +0200 Subject: [PATCH 03/17] Fix typo --- tests/e2e_tests/test_conviction_display.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e_tests/test_conviction_display.py b/tests/e2e_tests/test_conviction_display.py index 08c90e71f..56044686a 100644 --- a/tests/e2e_tests/test_conviction_display.py +++ b/tests/e2e_tests/test_conviction_display.py @@ -13,7 +13,7 @@ def _close(a, b): return math.isclose(a, b, rel_tol=0.0001, abs_tol=500) -def test_lock_roll_forward_comparision(local_chain, wallet_setup): +def test_lock_roll_forward_comparison(local_chain, wallet_setup): """ Test the accuracy of the lock roll-forward math between the chain and Btcli. From 6eaf8fe5d21d666a3cec62f4ef3ac5320173124f Mon Sep 17 00:00:00 2001 From: BD Himes Date: Mon, 8 Jun 2026 18:35:49 +0200 Subject: [PATCH 04/17] Remove liquidity commands and references --- bittensor_cli/cli.py | 325 -------- bittensor_cli/src/__init__.py | 16 +- .../src/commands/liquidity/__init__.py | 0 .../src/commands/liquidity/liquidity.py | 696 ------------------ bittensor_cli/src/commands/liquidity/utils.py | 202 ----- tests/e2e_tests/test_liquidity.py | 348 --------- 6 files changed, 3 insertions(+), 1584 deletions(-) delete mode 100644 bittensor_cli/src/commands/liquidity/__init__.py delete mode 100644 bittensor_cli/src/commands/liquidity/liquidity.py delete mode 100644 bittensor_cli/src/commands/liquidity/utils.py delete mode 100644 tests/e2e_tests/test_liquidity.py diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 86ff00b15..dfa7caf97 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -77,7 +77,6 @@ ) from bittensor_cli.src.commands import sudo, wallets, view from bittensor_cli.src.commands import weights as weights_cmds -from bittensor_cli.src.commands.liquidity import liquidity from bittensor_cli.src.commands.crowd import ( contribute as crowd_contribute, create as create_crowdloan, @@ -87,10 +86,6 @@ refund as crowd_refund, contributors as crowd_contributors, ) -from bittensor_cli.src.commands.liquidity.utils import ( - prompt_liquidity, - prompt_position_id, -) from bittensor_cli.src.commands import proxy as proxy_commands from bittensor_cli.src.commands.proxy import ProxyType from bittensor_cli.src.commands.stake import ( @@ -884,7 +879,6 @@ def __init__(self): self.subnet_mechanisms_app = typer.Typer(epilog=_epilog) self.weights_app = typer.Typer(epilog=_epilog) self.view_app = typer.Typer(epilog=_epilog) - self.liquidity_app = typer.Typer(epilog=_epilog) self.crowd_app = typer.Typer(epilog=_epilog) self.utils_app = typer.Typer(epilog=_epilog) self.axon_app = typer.Typer(epilog=_epilog) @@ -1394,30 +1388,6 @@ def __init__(self): "dissolve", rich_help_panel=HELP_PANELS["CROWD"]["INITIATOR"] )(self.crowd_dissolve) - # Liquidity - self.app.add_typer( - self.liquidity_app, - name="liquidity", - short_help="liquidity commands, aliases: `l`", - no_args_is_help=True, - ) - self.app.add_typer( - self.liquidity_app, name="l", hidden=True, no_args_is_help=True - ) - # liquidity commands - self.liquidity_app.command( - "add", rich_help_panel=HELP_PANELS["LIQUIDITY"]["LIQUIDITY_MGMT"] - )(self.liquidity_add) - self.liquidity_app.command( - "list", rich_help_panel=HELP_PANELS["LIQUIDITY"]["LIQUIDITY_MGMT"] - )(self.liquidity_list) - self.liquidity_app.command( - "modify", rich_help_panel=HELP_PANELS["LIQUIDITY"]["LIQUIDITY_MGMT"] - )(self.liquidity_modify) - self.liquidity_app.command( - "remove", rich_help_panel=HELP_PANELS["LIQUIDITY"]["LIQUIDITY_MGMT"] - )(self.liquidity_remove) - # utils app self.utils_app.command("convert")(self.convert) self.utils_app.command("latency")(self.best_connection) @@ -9060,301 +9030,6 @@ def view_dashboard( ) ) - # Liquidity - - def liquidity_add( - self, - network: Optional[list[str]] = Options.network, - wallet_name: str = Options.wallet_name, - wallet_path: str = Options.wallet_path, - wallet_hotkey: str = Options.wallet_hotkey, - netuid: Optional[int] = Options.netuid, - proxy: Optional[str] = Options.proxy, - liquidity_: Optional[float] = typer.Option( - None, - "--liquidity", - help="Amount of liquidity to add to the subnet.", - ), - price_low: Optional[float] = typer.Option( - None, - "--price-low", - "--price_low", - "--liquidity-price-low", - "--liquidity_price_low", - help="Low price for the adding liquidity position.", - ), - price_high: Optional[float] = typer.Option( - None, - "--price-high", - "--price_high", - "--liquidity-price-high", - "--liquidity_price_high", - help="High price for the adding liquidity position.", - ), - prompt: bool = Options.prompt, - decline: bool = Options.decline, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - json_output: bool = Options.json_output, - ): - """Add liquidity to the swap (as a combination of TAO + Alpha).""" - self.verbosity_handler(quiet, verbose, json_output, prompt, decline) - proxy = self.is_valid_proxy_name_or_ss58(proxy, False) - if not netuid: - netuid = Prompt.ask( - f"Enter the [{COLORS.G.SUBHEAD_MAIN}]netuid[/{COLORS.G.SUBHEAD_MAIN}] to use", - default=None, - show_default=False, - ) - - wallet, hotkey = self.wallet_ask( - wallet_name=wallet_name, - wallet_path=wallet_path, - wallet_hotkey=wallet_hotkey, - ask_for=[WO.NAME, WO.HOTKEY, WO.PATH], - validate=WV.WALLET, - return_wallet_and_hotkey=True, - ) - # Determine the liquidity amount. - if liquidity_: - liquidity_ = Balance.from_tao(liquidity_) - else: - liquidity_ = prompt_liquidity("Enter the amount of liquidity") - - # Determine price range - if price_low: - price_low = Balance.from_tao(price_low) - else: - price_low = prompt_liquidity("Enter liquidity position low price") - - if price_high: - price_high = Balance.from_tao(price_high) - else: - price_high = prompt_liquidity( - "Enter liquidity position high price (must be greater than low price)" - ) - - if price_low >= price_high: - print_error("The low price must be lower than the high price.") - return False - logger.debug( - f"args:\n" - f"hotkey: {type(hotkey)}\n" - f"netuid: {netuid}\n" - f"liquidity: {liquidity_}\n" - f"price_low: {price_low}\n" - f"price_high: {price_high}\n" - f"proxy: {type(proxy)}\n" - ) - return self._run_command( - liquidity.add_liquidity( - subtensor=self.initialize_chain(network), - wallet=wallet, - hotkey_ss58=hotkey, - netuid=netuid, - proxy=proxy, - liquidity=liquidity_, - price_low=price_low, - price_high=price_high, - prompt=prompt, - decline=decline, - quiet=quiet, - json_output=json_output, - ) - ) - - def liquidity_list( - self, - network: Optional[list[str]] = Options.network, - wallet_name: str = Options.wallet_name, - wallet_path: str = Options.wallet_path, - wallet_hotkey: str = Options.wallet_hotkey, - netuid: Optional[int] = Options.netuid, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - json_output: bool = Options.json_output, - ): - """Displays liquidity positions in given subnet.""" - self.verbosity_handler(quiet, verbose, json_output, prompt=False) - if not netuid: - netuid = IntPrompt.ask( - f"Enter the [{COLORS.G.SUBHEAD_MAIN}]netuid[/{COLORS.G.SUBHEAD_MAIN}] to use", - default=None, - show_default=False, - ) - - wallet = self.wallet_ask( - wallet_name=wallet_name, - wallet_path=wallet_path, - wallet_hotkey=wallet_hotkey, - ask_for=[WO.NAME, WO.PATH], - validate=WV.WALLET, - ) - self._run_command( - liquidity.show_liquidity_list( - subtensor=self.initialize_chain(network), - wallet=wallet, - netuid=netuid, - json_output=json_output, - ) - ) - - def liquidity_remove( - self, - network: Optional[list[str]] = Options.network, - wallet_name: str = Options.wallet_name, - wallet_path: str = Options.wallet_path, - wallet_hotkey: str = Options.wallet_hotkey, - netuid: Optional[int] = Options.netuid, - proxy: Optional[str] = Options.proxy, - position_id: Optional[int] = typer.Option( - None, - "--position-id", - "--position_id", - help="Position ID for modification or removal.", - ), - all_liquidity_ids: Optional[bool] = typer.Option( - False, - "--all", - "--a", - help="Whether to remove all liquidity positions for given subnet.", - ), - prompt: bool = Options.prompt, - decline: bool = Options.decline, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - json_output: bool = Options.json_output, - ): - """Remove liquidity from the swap (as a combination of TAO + Alpha).""" - - self.verbosity_handler(quiet, verbose, json_output, prompt, decline) - proxy = self.is_valid_proxy_name_or_ss58(proxy, False) - if all_liquidity_ids and position_id: - print_error("Cannot specify both --all and --position-id.") - return - - if not position_id and not all_liquidity_ids: - position_id = prompt_position_id() - - if not netuid: - netuid = IntPrompt.ask( - f"Enter the [{COLORS.G.SUBHEAD_MAIN}]netuid[/{COLORS.G.SUBHEAD_MAIN}] to use", - default=None, - show_default=False, - ) - - wallet, hotkey = self.wallet_ask( - wallet_name=wallet_name, - wallet_path=wallet_path, - wallet_hotkey=wallet_hotkey, - ask_for=[WO.NAME, WO.HOTKEY, WO.PATH], - validate=WV.WALLET, - return_wallet_and_hotkey=True, - ) - logger.debug( - f"args:\n" - f"network: {network}\n" - f"hotkey: {type(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), - wallet=wallet, - hotkey_ss58=hotkey, - netuid=netuid, - proxy=proxy, - position_id=position_id, - prompt=prompt, - decline=decline, - quiet=quiet, - all_liquidity_ids=all_liquidity_ids, - json_output=json_output, - ) - ) - - def liquidity_modify( - self, - network: Optional[list[str]] = Options.network, - wallet_name: str = Options.wallet_name, - wallet_path: str = Options.wallet_path, - wallet_hotkey: str = Options.wallet_hotkey, - netuid: Optional[int] = Options.netuid, - proxy: Optional[str] = Options.proxy, - position_id: Optional[int] = typer.Option( - None, - "--position-id", - "--position_id", - help="Position ID for modification or removing.", - ), - liquidity_delta: Optional[float] = typer.Option( - None, - "--liquidity-delta", - "--liquidity_delta", - help="Liquidity amount for modification.", - ), - prompt: bool = Options.prompt, - decline: bool = Options.decline, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - json_output: bool = Options.json_output, - ): - """Modifies the liquidity position for the given subnet.""" - self.verbosity_handler(quiet, verbose, json_output, prompt, decline) - proxy = self.is_valid_proxy_name_or_ss58(proxy, False) - if not netuid: - netuid = IntPrompt.ask( - f"Enter the [{COLORS.G.SUBHEAD_MAIN}]netuid[/{COLORS.G.SUBHEAD_MAIN}] to use", - ) - - wallet, hotkey = self.wallet_ask( - wallet_name=wallet_name, - wallet_path=wallet_path, - wallet_hotkey=wallet_hotkey, - ask_for=[WO.NAME, WO.HOTKEY, WO.PATH], - validate=WV.WALLET, - return_wallet_and_hotkey=True, - ) - - if not position_id: - position_id = prompt_position_id() - - if liquidity_delta: - liquidity_delta = Balance.from_tao(liquidity_delta) - else: - liquidity_delta = prompt_liquidity( - f"Enter the [blue]liquidity delta[/blue] to modify position with id " - f"[blue]{position_id}[/blue] (can be positive or negative)", - negative_allowed=True, - ) - logger.debug( - f"args:\n" - f"network: {network}\n" - f"hotkey: {type(hotkey)}\n" - f"netuid: {netuid}\n" - f"position_id: {position_id}\n" - f"liquidity_delta: {liquidity_delta}\n" - f"proxy: {type(proxy)}\n" - ) - - return self._run_command( - liquidity.modify_liquidity( - subtensor=self.initialize_chain(network), - wallet=wallet, - hotkey_ss58=hotkey, - netuid=netuid, - proxy=proxy, - position_id=position_id, - liquidity_delta=liquidity_delta, - prompt=prompt, - decline=decline, - quiet=quiet, - json_output=json_output, - ) - ) - # Crowd def crowd_list( diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 0b2da85a9..779318dd5 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -618,7 +618,6 @@ class RootSudoOnly(Enum): ), "yuma3_enabled": ("sudo_set_yuma3_enabled", RootSudoOnly.FALSE), "alpha_sigmoid_steepness": ("sudo_set_alpha_sigmoid_steepness", RootSudoOnly.TRUE), - "user_liquidity_enabled": ("toggle_user_liquidity", RootSudoOnly.COMPLICATED), "bonds_reset_enabled": ("sudo_set_bonds_reset_enabled", RootSudoOnly.FALSE), "transfers_enabled": ("sudo_set_toggle_transfer", RootSudoOnly.FALSE), "min_allowed_uids": ("sudo_set_min_allowed_uids", RootSudoOnly.TRUE), @@ -642,9 +641,9 @@ class RootSudoOnly(Enum): "burn_half_life": ("sudo_set_burn_half_life", RootSudoOnly.FALSE), } -HYPERPARAMS_MODULE = { - "user_liquidity_enabled": "Swap", -} +# Maps a hyperparameter to a non-default pallet for sudo set calls. Empty by default +# (all current hyperparameters live in the default pallet). +HYPERPARAMS_MODULE: dict[str, str] = {} # Hyperparameter metadata: descriptions, side-effects, ownership, and documentation links HYPERPARAMS_METADATA = { @@ -780,12 +779,6 @@ class RootSudoOnly(Enum): "owner_settable": False, "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#alphasigmoidsteepness", }, - "user_liquidity_enabled": { - "description": "Enable or disable user liquidity features.", - "side_effects": "Enabling allows liquidity provision and swaps. Disabling restricts liquidity operations.", - "owner_settable": True, # COMPLICATED - can be set by owner or sudo - "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#userliquidityenabled", - }, "bonds_reset_enabled": { "description": "Enable or disable periodic bond resets.", "side_effects": "Enabling provides periodic bond resets, preventing bond accumulation. Disabling allows bonds to accumulate.", @@ -921,9 +914,6 @@ class RootSudoOnly(Enum): "VIEW": { "DASHBOARD": "Network Dashboard", }, - "LIQUIDITY": { - "LIQUIDITY_MGMT": "Liquidity Management", - }, "CROWD": { "INITIATOR": "Crowdloan Creation & Management", "PARTICIPANT": "Crowdloan Participation", diff --git a/bittensor_cli/src/commands/liquidity/__init__.py b/bittensor_cli/src/commands/liquidity/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/bittensor_cli/src/commands/liquidity/liquidity.py b/bittensor_cli/src/commands/liquidity/liquidity.py deleted file mode 100644 index 32a3844ca..000000000 --- a/bittensor_cli/src/commands/liquidity/liquidity.py +++ /dev/null @@ -1,696 +0,0 @@ -import asyncio -import json -from typing import TYPE_CHECKING, Optional - -from async_substrate_interface import AsyncExtrinsicReceipt -from rich.table import Column, Table - -from bittensor_cli.src import COLORS -from bittensor_cli.src.bittensor.utils import ( - confirm_action, - unlock_key, - console, - print_error, - json_console, - print_extrinsic_id, -) -from bittensor_cli.src.bittensor.balances import Balance, fixed_to_float -from bittensor_cli.src.commands.liquidity.utils import ( - LiquidityPosition, - calculate_fees, - get_fees, - price_to_tick, - tick_to_price, -) - -if TYPE_CHECKING: - from bittensor_wallet import Wallet - from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface - - -async def add_liquidity_extrinsic( - subtensor: "SubtensorInterface", - wallet: "Wallet", - hotkey_ss58: str, - netuid: int, - proxy: Optional[str], - liquidity: Balance, - price_low: Balance, - price_high: Balance, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, -) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: - """ - Adds liquidity to the specified price range. - - Arguments: - subtensor: The Subtensor client instance used for blockchain interaction. - wallet: The wallet used to sign the extrinsic (must be unlocked). - hotkey_ss58: the SS58 of the hotkey to use for this transaction. - netuid: The UID of the target subnet for which the call is being initiated. - proxy: Optional proxy to use for this extrinsic submission. - liquidity: The amount of liquidity to be added. - price_low: The lower bound of the price tick range. - price_high: The upper bound of the price tick range. - wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. Defaults to True. - wait_for_finalization: Whether to wait for finalization of the extrinsic. Defaults to False. - - Returns: - tuple: - bool: True if successful, False otherwise. - str: success message if successful, error message otherwise. - AsyncExtrinsicReceipt: extrinsic receipt if successful, None otherwise. - - Note: Adding is allowed even when user liquidity is enabled in specified subnet. Call - `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. - """ - if not (unlock := unlock_key(wallet)).success: - return False, unlock.message, None - - tick_low = price_to_tick(price_low.tao) - tick_high = price_to_tick(price_high.tao) - - call = await subtensor.substrate.compose_call( - call_module="Swap", - call_function="add_liquidity", - call_params={ - "hotkey": hotkey_ss58, - "netuid": netuid, - "tick_low": tick_low, - "tick_high": tick_high, - "liquidity": liquidity.rao, - }, - ) - - return await subtensor.sign_and_send_extrinsic( - call=call, - wallet=wallet, - proxy=proxy, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - -async def modify_liquidity_extrinsic( - subtensor: "SubtensorInterface", - wallet: "Wallet", - hotkey_ss58: str, - netuid: int, - proxy: Optional[str], - position_id: int, - liquidity_delta: Balance, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, -) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: - """Modifies liquidity in liquidity position by adding or removing liquidity from it. - - Arguments: - subtensor: The Subtensor client instance used for blockchain interaction. - wallet: The wallet used to sign the extrinsic (must be unlocked). - hotkey_ss58: the SS58 of the hotkey to use for this transaction. - netuid: The UID of the target subnet for which the call is being initiated. - proxy: Optional proxy to use for this extrinsic submission. - position_id: The id of the position record in the pool. - liquidity_delta: The amount of liquidity to be added or removed (add if positive or remove if negative). - wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. Defaults to True. - wait_for_finalization: Whether to wait for finalization of the extrinsic. Defaults to False. - - Returns: - tuple: - bool: True if successful, False otherwise. - str: success message if successful, error message otherwise. - AsyncExtrinsicReceipt: extrinsic receipt if successful, None otherwise. - - Note: Modifying is allowed even when user liquidity is enabled in specified subnet. - Call `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. - """ - if not (unlock := unlock_key(wallet)).success: - return False, unlock.message, None - - call = await subtensor.substrate.compose_call( - call_module="Swap", - call_function="modify_position", - call_params={ - "hotkey": hotkey_ss58, - "netuid": netuid, - "position_id": position_id, - "liquidity_delta": liquidity_delta.rao, - }, - ) - - return await subtensor.sign_and_send_extrinsic( - call=call, - wallet=wallet, - proxy=proxy, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - -async def remove_liquidity_extrinsic( - subtensor: "SubtensorInterface", - wallet: "Wallet", - hotkey_ss58: str, - proxy: Optional[str], - netuid: int, - position_id: int, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, -) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: - """Remove liquidity and credit balances back to wallet's hotkey stake. - - Arguments: - subtensor: The Subtensor client instance used for blockchain interaction. - wallet: The wallet used to sign the extrinsic (must be unlocked). - hotkey_ss58: the SS58 of the hotkey to use for this transaction. - proxy: Optional proxy to use for this extrinsic submission. - netuid: The UID of the target subnet for which the call is being initiated. - position_id: The id of the position record in the pool. - wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. Defaults to True. - wait_for_finalization: Whether to wait for finalization of the extrinsic. Defaults to False. - - Returns: - tuple: - bool: True if successful, False otherwise. - str: success message if successful, error message otherwise. - AsyncExtrinsicReceipt: extrinsic receipt if successful, None otherwise. - - Note: Adding is allowed even when user liquidity is enabled in specified subnet. - Call `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. - """ - if not (unlock := unlock_key(wallet)).success: - return False, unlock.message, None - - call = await subtensor.substrate.compose_call( - call_module="Swap", - call_function="remove_liquidity", - call_params={ - "hotkey": hotkey_ss58, - "netuid": netuid, - "position_id": position_id, - }, - ) - - return await subtensor.sign_and_send_extrinsic( - call=call, - wallet=wallet, - proxy=proxy, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - -async def toggle_user_liquidity_extrinsic( - subtensor: "SubtensorInterface", - wallet: "Wallet", - netuid: int, - enable: bool, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, -) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: - """Allow to toggle user liquidity for specified subnet. - - Arguments: - subtensor: The Subtensor client instance used for blockchain interaction. - wallet: The wallet used to sign the extrinsic (must be unlocked). - netuid: The UID of the target subnet for which the call is being initiated. - enable: Boolean indicating whether to enable user liquidity. - wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. Defaults to True. - wait_for_finalization: Whether to wait for finalization of the extrinsic. Defaults to False. - - Returns: - Tuple[bool, str]: - - True and a success message if the extrinsic is successfully submitted or processed. - - False and an error message if the submission fails or the wallet cannot be unlocked. - """ - if not (unlock := unlock_key(wallet)).success: - return False, unlock.message, None - - call = await subtensor.substrate.compose_call( - call_module="Swap", - call_function="toggle_user_liquidity", - call_params={"netuid": netuid, "enable": enable}, - ) - - return await subtensor.sign_and_send_extrinsic( - call=call, - wallet=wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - -# Command -async def add_liquidity( - subtensor: "SubtensorInterface", - wallet: "Wallet", - hotkey_ss58: str, - netuid: Optional[int], - proxy: Optional[str], - liquidity: Balance, - price_low: Balance, - price_high: Balance, - prompt: bool, - decline: bool, - quiet: bool, - json_output: bool, -) -> tuple[bool, str]: - """Add liquidity position to provided subnet.""" - # Check wallet access - if not (ulw := unlock_key(wallet)).success: - return False, ulw.message - - # Check that the subnet exists. - if not await subtensor.subnet_exists(netuid=netuid): - return False, f"Subnet with netuid: {netuid} does not exist in {subtensor}." - - if prompt: - console.print( - "You are about to add a LiquidityPosition with:\n" - f"\tliquidity: {liquidity}\n" - f"\tprice low: {price_low}\n" - f"\tprice high: {price_high}\n" - f"\tto SN: {netuid}\n" - f"\tusing wallet with name: {wallet.name}" - ) - - if not confirm_action( - "Would you like to continue?", decline=decline, quiet=quiet - ): - return False, "User cancelled operation." - - success, message, ext_receipt = await add_liquidity_extrinsic( - subtensor=subtensor, - wallet=wallet, - hotkey_ss58=hotkey_ss58, - netuid=netuid, - proxy=proxy, - liquidity=liquidity, - price_low=price_low, - price_high=price_high, - ) - if success: - await print_extrinsic_id(ext_receipt) - ext_id = await ext_receipt.get_extrinsic_identifier() - else: - ext_id = None - if json_output: - json_console.print_json( - data={ - "success": success, - "message": message, - "extrinsic_identifier": ext_id, - } - ) - else: - if success: - console.print( - "[green]LiquidityPosition has been successfully added.[/green]" - ) - else: - print_error(f"Error: {message}") - return success, message - - -async def get_liquidity_list( - subtensor: "SubtensorInterface", - wallet: "Wallet", - netuid: Optional[int], -) -> tuple[bool, str, list]: - """ - Args: - wallet: wallet object - subtensor: SubtensorInterface object - netuid: the netuid to stake to (None indicates all subnets) - - Returns: - Tuple of (success, error message, liquidity list) - """ - - if not await subtensor.subnet_exists(netuid=netuid): - return False, f"Subnet with netuid: {netuid} does not exist in {subtensor}.", [] - - if not await subtensor.is_subnet_active(netuid=netuid): - return False, f"Subnet with netuid: {netuid} is not active in {subtensor}.", [] - - block_hash = await subtensor.substrate.get_chain_head() - ( - positions_response, - fee_global_tao, - fee_global_alpha, - current_sqrt_price, - ) = await asyncio.gather( - subtensor.substrate.query_map( - module="Swap", - storage_function="Positions", - params=[netuid, wallet.coldkeypub.ss58_address], - block_hash=block_hash, - ), - subtensor.query( - module="Swap", - storage_function="FeeGlobalTao", - params=[netuid], - block_hash=block_hash, - ), - subtensor.query( - module="Swap", - storage_function="FeeGlobalAlpha", - params=[netuid], - block_hash=block_hash, - ), - subtensor.query( - module="Swap", - storage_function="AlphaSqrtPrice", - params=[netuid], - block_hash=block_hash, - ), - ) - if len(positions_response.records) == 0: - return False, "No liquidity positions found.", [] - - current_sqrt_price = fixed_to_float(current_sqrt_price) - fee_global_tao = fixed_to_float(fee_global_tao) - fee_global_alpha = fixed_to_float(fee_global_alpha) - - current_price = current_sqrt_price * current_sqrt_price - current_tick = price_to_tick(current_price) - - preprocessed_positions = [] - positions_futures = [] - - async for _, position in positions_response: - tick_index_low = position.get("tick_low") - tick_index_high = position.get("tick_high") - preprocessed_positions.append((position, tick_index_low, tick_index_high)) - - # Get ticks for the position (for below/above fees) - positions_futures.append( - asyncio.gather( - subtensor.query( - module="Swap", - storage_function="Ticks", - params=[netuid, tick_index_low], - block_hash=block_hash, - ), - subtensor.query( - module="Swap", - storage_function="Ticks", - params=[netuid, tick_index_high], - block_hash=block_hash, - ), - ) - ) - - awaited_futures = await asyncio.gather(*positions_futures) - - positions = [] - - for (position, tick_index_low, tick_index_high), (tick_low, tick_high) in zip( - preprocessed_positions, awaited_futures - ): - tao_fees_below_low = get_fees( - current_tick=current_tick, - tick=tick_low, - tick_index=tick_index_low, - quote=True, - global_fees_tao=fee_global_tao, - global_fees_alpha=fee_global_alpha, - above=False, - ) - tao_fees_above_high = get_fees( - current_tick=current_tick, - tick=tick_high, - tick_index=tick_index_high, - quote=True, - global_fees_tao=fee_global_tao, - global_fees_alpha=fee_global_alpha, - above=True, - ) - alpha_fees_below_low = get_fees( - current_tick=current_tick, - tick=tick_low, - tick_index=tick_index_low, - quote=False, - global_fees_tao=fee_global_tao, - global_fees_alpha=fee_global_alpha, - above=False, - ) - alpha_fees_above_high = get_fees( - current_tick=current_tick, - tick=tick_high, - tick_index=tick_index_high, - quote=False, - global_fees_tao=fee_global_tao, - global_fees_alpha=fee_global_alpha, - above=True, - ) - - # Get position accrued fees - fees_tao, fees_alpha = calculate_fees( - position=position, - global_fees_tao=fee_global_tao, - global_fees_alpha=fee_global_alpha, - tao_fees_below_low=tao_fees_below_low, - tao_fees_above_high=tao_fees_above_high, - alpha_fees_below_low=alpha_fees_below_low, - alpha_fees_above_high=alpha_fees_above_high, - netuid=netuid, - ) - - lp = LiquidityPosition( - **{ - "id": position.get("id"), - "price_low": Balance.from_tao(tick_to_price(position.get("tick_low"))), - "price_high": Balance.from_tao( - tick_to_price(position.get("tick_high")) - ), - "liquidity": Balance.from_rao(position.get("liquidity")), - "fees_tao": fees_tao, - "fees_alpha": fees_alpha, - "netuid": position.get("netuid"), - } - ) - positions.append(lp) - - return True, "", positions - - -async def show_liquidity_list( - subtensor: "SubtensorInterface", - wallet: "Wallet", - netuid: int, - json_output: bool = False, -) -> None: - current_price_, liquidity_list_ = await asyncio.gather( - subtensor.subnet(netuid=netuid), - get_liquidity_list(subtensor, wallet, netuid), - return_exceptions=True, - ) - if isinstance(current_price_, Exception): - success = False - err_msg = str(current_price_) - positions = [] - elif isinstance(liquidity_list_, Exception): - success = False - err_msg = str(liquidity_list_) - positions = [] - else: - (success, err_msg, positions) = liquidity_list_ - if not success: - if json_output: - json_console.print( - json.dumps({"success": success, "err_msg": err_msg, "positions": []}) - ) - return - else: - print_error(f"Error: {err_msg}") - return - liquidity_table = Table( - Column("ID", justify="center"), - Column("Liquidity", justify="center"), - Column("Alpha", justify="center"), - Column("Tao", justify="center"), - Column("Price low", justify="center"), - Column("Price high", justify="center"), - Column("Fee TAO", justify="center"), - Column("Fee Alpha", justify="center"), - title=f"\n[{COLORS.G.HEADER}]{'Liquidity Positions of '}{wallet.name} wallet in SN #{netuid}\n" - "Alpha and Tao columns are respective portions of liquidity.", - show_footer=False, - show_edge=True, - header_style="bold white", - border_style="bright_black", - style="bold", - title_justify="center", - show_lines=False, - pad_edge=True, - ) - json_table = [] - current_price = current_price_.price - lp: LiquidityPosition - for lp in positions: - alpha, tao = lp.to_token_amounts(current_price) - liquidity_table.add_row( - str(lp.id), - str(lp.liquidity.tao), - str(alpha), - str(tao), - str(lp.price_low), - str(lp.price_high), - str(lp.fees_tao), - str(lp.fees_alpha), - ) - json_table.append( - { - "id": lp.id, - "liquidity": lp.liquidity.tao, - "token_amounts": {"alpha": alpha.tao, "tao": tao.tao}, - "price_low": lp.price_low.tao, - "price_high": lp.price_high.tao, - "fees_tao": lp.fees_tao.tao, - "fees_alpha": lp.fees_alpha.tao, - "netuid": lp.netuid, - } - ) - if not json_output: - console.print(liquidity_table) - else: - json_console.print( - json.dumps({"success": True, "err_msg": "", "positions": json_table}) - ) - - -async def remove_liquidity( - subtensor: "SubtensorInterface", - wallet: "Wallet", - hotkey_ss58: str, - netuid: int, - proxy: Optional[str], - position_id: Optional[int] = None, - prompt: Optional[bool] = None, - decline: bool = False, - quiet: bool = False, - all_liquidity_ids: Optional[bool] = None, - json_output: bool = False, -) -> None: - """Remove liquidity position from provided subnet.""" - if not await subtensor.subnet_exists(netuid=netuid): - return None - - if all_liquidity_ids: - success, msg, positions = await get_liquidity_list(subtensor, wallet, netuid) - if not success: - if json_output: - json_console.print_json( - data={"success": False, "err_msg": msg, "positions": positions} - ) - else: - return print_error(f"Error: {msg}") - return None - else: - position_ids = [p.id for p in positions] - else: - position_ids = [position_id] - - if prompt: - console.print("You are about to remove LiquidityPositions with:") - console.print(f"\tSubnet: {netuid}") - console.print(f"\tWallet name: {wallet.name}") - for pos in position_ids: - console.print(f"\tPosition id: {pos}") - - if not confirm_action( - "Would you like to continue?", decline=decline, quiet=quiet - ): - return None - - # TODO does this never break because of the nonce? - results = await asyncio.gather( - *[ - remove_liquidity_extrinsic( - subtensor=subtensor, - wallet=wallet, - hotkey_ss58=hotkey_ss58, - proxy=proxy, - netuid=netuid, - position_id=pos_id, - ) - for pos_id in position_ids - ] - ) - if not json_output: - for (success, msg, ext_receipt), posid in zip(results, position_ids): - if success: - await print_extrinsic_id(ext_receipt) - console.print(f"[green] Position {posid} has been removed.") - else: - print_error(f"Error removing {posid}: {msg}") - else: - json_table = {} - for (success, msg, ext_receipt), posid in zip(results, position_ids): - json_table[posid] = { - "success": success, - "err_msg": msg, - "extrinsic_identifier": await ext_receipt.get_extrinsic_identifier(), - } - json_console.print_json(data=json_table) - return None - - -async def modify_liquidity( - subtensor: "SubtensorInterface", - wallet: "Wallet", - hotkey_ss58: str, - netuid: int, - proxy: Optional[str], - position_id: int, - liquidity_delta: Balance, - prompt: Optional[bool] = None, - decline: bool = False, - quiet: bool = False, - json_output: bool = False, -) -> bool: - """Modify liquidity position in provided subnet.""" - if not await subtensor.subnet_exists(netuid=netuid): - err_msg = f"Subnet with netuid: {netuid} does not exist in {subtensor}." - if json_output: - json_console.print(json.dumps({"success": False, "err_msg": err_msg})) - else: - print_error(err_msg) - return False - - if prompt: - console.print( - "You are about to modify a LiquidityPosition with:" - f"\tSubnet: {netuid}\n" - f"\tPosition id: {position_id}\n" - f"\tWallet name: {wallet.name}\n" - f"\tLiquidity delta: {liquidity_delta}" - ) - - if not confirm_action( - "Would you like to continue?", decline=decline, quiet=quiet - ): - return False - - success, msg, ext_receipt = await modify_liquidity_extrinsic( - subtensor=subtensor, - wallet=wallet, - hotkey_ss58=hotkey_ss58, - netuid=netuid, - proxy=proxy, - position_id=position_id, - liquidity_delta=liquidity_delta, - ) - if json_output: - ext_id = await ext_receipt.get_extrinsic_identifier() if success else None - json_console.print_json( - data={"success": success, "err_msg": msg, "extrinsic_identifier": ext_id} - ) - else: - if success: - await print_extrinsic_id(ext_receipt) - console.print(f"[green] Position {position_id} has been modified.") - else: - print_error(f"Error modifying {position_id}: {msg}") - return success diff --git a/bittensor_cli/src/commands/liquidity/utils.py b/bittensor_cli/src/commands/liquidity/utils.py deleted file mode 100644 index f364a64e4..000000000 --- a/bittensor_cli/src/commands/liquidity/utils.py +++ /dev/null @@ -1,202 +0,0 @@ -""" -This module provides utilities for managing liquidity positions and price conversions in the Bittensor network. The -module handles conversions between TAO and Alpha tokens while maintaining precise calculations for liquidity -provisioning and fee distribution. -""" - -import math -from dataclasses import dataclass -from typing import Any - -from rich.prompt import IntPrompt, FloatPrompt - -from bittensor_cli.src.bittensor.balances import Balance, fixed_to_float -from bittensor_cli.src.bittensor.utils import ( - console, -) - -# These three constants are unchangeable at the level of Uniswap math -MIN_TICK = -887272 -MAX_TICK = 887272 -PRICE_STEP = 1.0001 - - -@dataclass -class LiquidityPosition: - id: int - price_low: Balance # RAO - price_high: Balance # RAO - liquidity: Balance # TAO + ALPHA (sqrt by TAO balance * Alpha Balance -> math under the hood) - fees_tao: Balance # RAO - fees_alpha: Balance # RAO - netuid: int - - def to_token_amounts( - self, current_subnet_price: Balance - ) -> tuple[Balance, Balance]: - """Convert a position to token amounts. - - Arguments: - current_subnet_price: current subnet price in Alpha. - - Returns: - tuple[int, int]: - Amount of Alpha in liquidity - Amount of TAO in liquidity - - Liquidity is a combination of TAO and Alpha depending on the price of the subnet at the moment. - """ - sqrt_price_low = math.sqrt(self.price_low) - sqrt_price_high = math.sqrt(self.price_high) - sqrt_current_subnet_price = math.sqrt(current_subnet_price) - - if sqrt_current_subnet_price < sqrt_price_low: - amount_alpha = self.liquidity * (1 / sqrt_price_low - 1 / sqrt_price_high) - amount_tao = 0 - elif sqrt_current_subnet_price > sqrt_price_high: - amount_alpha = 0 - amount_tao = self.liquidity * (sqrt_price_high - sqrt_price_low) - else: - amount_alpha = self.liquidity * ( - 1 / sqrt_current_subnet_price - 1 / sqrt_price_high - ) - amount_tao = self.liquidity * (sqrt_current_subnet_price - sqrt_price_low) - return Balance.from_rao(int(amount_alpha)).set_unit( - self.netuid - ), Balance.from_rao(int(amount_tao)) - - -def price_to_tick(price: float) -> int: - """Converts a float price to the nearest Uniswap V3 tick index.""" - if price <= 0: - raise ValueError(f"Price must be positive, got `{price}`.") - - tick = int(math.log(price) / math.log(PRICE_STEP)) - - if not (MIN_TICK <= tick <= MAX_TICK): - raise ValueError( - f"Resulting tick {tick} is out of allowed range ({MIN_TICK} to {MAX_TICK})" - ) - return tick - - -def tick_to_price(tick: int) -> float: - """Convert an integer Uniswap V3 tick index to float price.""" - if not (MIN_TICK <= tick <= MAX_TICK): - raise ValueError("Tick is out of allowed range") - return PRICE_STEP**tick - - -def get_fees( - current_tick: int, - tick: dict, - tick_index: int, - quote: bool, - global_fees_tao: float, - global_fees_alpha: float, - above: bool, -) -> float: - """Returns the liquidity fee.""" - tick_fee_key = "fees_out_tao" if quote else "fees_out_alpha" - tick_fee_value = fixed_to_float(tick.get(tick_fee_key)) - global_fee_value = global_fees_tao if quote else global_fees_alpha - - if above: - return ( - global_fee_value - tick_fee_value - if tick_index <= current_tick - else tick_fee_value - ) - return ( - tick_fee_value - if tick_index <= current_tick - else global_fee_value - tick_fee_value - ) - - -def get_fees_in_range( - quote: bool, - global_fees_tao: float, - global_fees_alpha: float, - fees_below_low: float, - fees_above_high: float, -) -> float: - """Returns the liquidity fee value in a range.""" - global_fees = global_fees_tao if quote else global_fees_alpha - return global_fees - fees_below_low - fees_above_high - - -# Calculate fees for a position -def calculate_fees( - position: dict[str, Any], - global_fees_tao: float, - global_fees_alpha: float, - tao_fees_below_low: float, - tao_fees_above_high: float, - alpha_fees_below_low: float, - alpha_fees_above_high: float, - netuid: int, -) -> tuple[Balance, Balance]: - fee_tao_agg = get_fees_in_range( - quote=True, - global_fees_tao=global_fees_tao, - global_fees_alpha=global_fees_alpha, - fees_below_low=tao_fees_below_low, - fees_above_high=tao_fees_above_high, - ) - - fee_alpha_agg = get_fees_in_range( - quote=False, - global_fees_tao=global_fees_tao, - global_fees_alpha=global_fees_alpha, - fees_below_low=alpha_fees_below_low, - fees_above_high=alpha_fees_above_high, - ) - - fee_tao = fee_tao_agg - fixed_to_float(position["fees_tao"]) - fee_alpha = fee_alpha_agg - fixed_to_float(position["fees_alpha"]) - liquidity_frac = position["liquidity"] - - fee_tao = liquidity_frac * fee_tao - fee_alpha = liquidity_frac * fee_alpha - - return Balance.from_rao(int(fee_tao)), Balance.from_rao(int(fee_alpha)).set_unit( - netuid - ) - - -def prompt_liquidity(prompt: str, negative_allowed: bool = False) -> Balance: - """Prompt the user for the amount of liquidity. - - Arguments: - prompt: Prompt to display to the user. - negative_allowed: Whether negative amounts are allowed. - - Returns: - Balance converted from input to TAO. - """ - while True: - amount = FloatPrompt.ask(prompt) - try: - if amount <= 0 and not negative_allowed: - console.print("[red]Amount must be greater than 0[/red].") - continue - return Balance.from_tao(amount) - except ValueError: - console.print("[red]Please enter a valid number[/red].") - - -def prompt_position_id() -> int: - """Ask the user for the ID of the liquidity position to remove.""" - while True: - position_id = IntPrompt.ask("Enter the [blue]liquidity position ID[/blue]") - - try: - if position_id <= 1: - console.print("[red]Position ID must be greater than 1[/red].") - continue - return position_id - except ValueError: - console.print("[red]Please enter a valid number[/red].") - # will never return this, but fixes the type checker - return 0 diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py deleted file mode 100644 index a5c38ccc8..000000000 --- a/tests/e2e_tests/test_liquidity.py +++ /dev/null @@ -1,348 +0,0 @@ -import pytest -import asyncio -import json -import time - -from .utils import turn_off_hyperparam_freeze_window - -""" -Verify commands: - -* btcli liquidity add -* btcli liquidity list -* btcli liquidity modify -* btcli liquidity remove -""" - - -@pytest.mark.skip(reason="User liquidity currently disabled on chain") -def test_liquidity(local_chain, wallet_setup): - wallet_path_alice = "//Alice" - netuid = 2 - - # Create wallet for Alice - keypair_alice, wallet_alice, wallet_path_alice, exec_command_alice = wallet_setup( - wallet_path_alice - ) - try: - asyncio.run(turn_off_hyperparam_freeze_window(local_chain, wallet_alice)) - except ValueError: - print( - "Skipping turning off hyperparams freeze window. This indicates the call does not exist on the chain you are testing." - ) - - # Register a subnet with sudo as Alice - result = exec_command_alice( - command="subnets", - sub_command="create", - extra_args=[ - "--wallet-path", - wallet_path_alice, - "--chain", - "ws://127.0.0.1:9945", - "--wallet-name", - wallet_alice.name, - "--wallet-hotkey", - wallet_alice.hotkey_str, - "--subnet-name", - "Test Subnet", - "--repo", - "https://github.com/username/repo", - "--contact", - "alice@opentensor.dev", - "--url", - "https://testsubnet.com", - "--discord", - "alice#1234", - "--description", - "A test subnet for e2e testing", - "--additional-info", - "Created by Alice", - "--logo-url", - "https://testsubnet.com/logo.png", - "--no-prompt", - "--json-output", - "--no-mev-protection", - ], - ) - result_output = json.loads(result.stdout) - assert result_output["success"] is True - assert result_output["netuid"] == netuid - assert isinstance(result_output["extrinsic_identifier"], str) - - # verify no results for list thus far (subnet not yet started) - liquidity_list_result = exec_command_alice( - command="liquidity", - sub_command="list", - extra_args=[ - "--wallet-path", - wallet_path_alice, - "--chain", - "ws://127.0.0.1:9945", - "--wallet-name", - wallet_alice.name, - "--wallet-hotkey", - wallet_alice.hotkey_str, - "--netuid", - netuid, - "--json-output", - ], - ) - result_output = json.loads(liquidity_list_result.stdout) - assert result_output["success"] is False - assert f"Subnet with netuid: {netuid} is not active" in result_output["err_msg"] - assert result_output["positions"] == [] - time.sleep(40) - - # start emissions schedule - start_subnet_emissions = exec_command_alice( - command="subnets", - sub_command="start", - extra_args=[ - "--netuid", - netuid, - "--wallet-path", - wallet_path_alice, - "--wallet-name", - wallet_alice.name, - "--hotkey", - wallet_alice.hotkey_str, - "--network", - "ws://127.0.0.1:9945", - "--no-prompt", - ], - ) - assert ( - f"Successfully started subnet {netuid}'s emission schedule" - in start_subnet_emissions.stdout - ), start_subnet_emissions.stderr - assert "Your extrinsic has been included " in start_subnet_emissions.stdout - - stake_to_enable_v3 = exec_command_alice( - command="stake", - sub_command="add", - extra_args=[ - "--netuid", - "2", - "--wallet-path", - wallet_path_alice, - "--wallet-name", - wallet_alice.name, - "--hotkey", - wallet_alice.hotkey_str, - "--chain", - "ws://127.0.0.1:9945", - "--amount", - "1", - "--unsafe", - "--no-prompt", - "--era", - "144", - "--no-mev-protection", - ], - ) - assert "✅ Finalized" in stake_to_enable_v3.stdout, stake_to_enable_v3.stderr - time.sleep(10) - liquidity_list_result = exec_command_alice( - command="liquidity", - sub_command="list", - extra_args=[ - "--wallet-path", - wallet_path_alice, - "--chain", - "ws://127.0.0.1:9945", - "--wallet-name", - wallet_alice.name, - "--wallet-hotkey", - wallet_alice.hotkey_str, - "--netuid", - netuid, - "--json-output", - ], - ) - print(">>>", liquidity_list_result.stdout, liquidity_list_result.stderr) - result_output = json.loads(liquidity_list_result.stdout) - assert result_output["success"] is False - assert result_output["err_msg"] == "No liquidity positions found." - assert result_output["positions"] == [] - - enable_user_liquidity = exec_command_alice( - command="sudo", - sub_command="set", - extra_args=[ - "--wallet-path", - wallet_path_alice, - "--chain", - "ws://127.0.0.1:9945", - "--wallet-name", - wallet_alice.name, - "--wallet-hotkey", - wallet_alice.hotkey_str, - "--netuid", - netuid, - "--param", - "user_liquidity_enabled", - "--value", - "1", - "--json-output", - "--no-prompt", - ], - ) - enable_user_liquidity_result = json.loads(enable_user_liquidity.stdout) - assert enable_user_liquidity_result["success"] is True - assert isinstance(enable_user_liquidity_result["extrinsic_identifier"], str) - - add_liquidity = exec_command_alice( - command="liquidity", - sub_command="add", - extra_args=[ - "--wallet-path", - wallet_path_alice, - "--chain", - "ws://127.0.0.1:9945", - "--wallet-name", - wallet_alice.name, - "--wallet-hotkey", - wallet_alice.hotkey_str, - "--netuid", - netuid, - "--liquidity", - "1.0", - "--price-low", - "1.7", - "--price-high", - "1.8", - "--no-prompt", - "--json-output", - ], - ) - add_liquidity_result = json.loads(add_liquidity.stdout) - assert add_liquidity_result["success"] is True - assert add_liquidity_result["message"] == "" - assert isinstance(add_liquidity_result["extrinsic_identifier"], str) - - liquidity_list_result = exec_command_alice( - command="liquidity", - sub_command="list", - extra_args=[ - "--wallet-path", - wallet_path_alice, - "--chain", - "ws://127.0.0.1:9945", - "--wallet-name", - wallet_alice.name, - "--wallet-hotkey", - wallet_alice.hotkey_str, - "--netuid", - netuid, - "--json-output", - ], - ) - print(">>>", liquidity_list_result.stdout, liquidity_list_result.stderr) - liquidity_list_result = json.loads(liquidity_list_result.stdout) - assert liquidity_list_result["success"] is True - assert len(liquidity_list_result["positions"]) == 1 - liquidity_position = liquidity_list_result["positions"][0] - assert liquidity_position["liquidity"] == 1.0 - assert liquidity_position["fees_tao"] == 0.0 - assert liquidity_position["fees_alpha"] == 0.0 - assert liquidity_position["netuid"] == netuid - assert abs(liquidity_position["price_high"] - 1.8) < 0.0001 - assert abs(liquidity_position["price_low"] - 1.7) < 0.0001 - - modify_liquidity = exec_command_alice( - command="liquidity", - sub_command="modify", - extra_args=[ - "--wallet-path", - wallet_path_alice, - "--chain", - "ws://127.0.0.1:9945", - "--wallet-name", - wallet_alice.name, - "--wallet-hotkey", - wallet_alice.hotkey_str, - "--netuid", - netuid, - "--position-id", - str(liquidity_position["id"]), - "--liquidity-delta", - "20.0", - "--json-output", - "--no-prompt", - ], - ) - modify_liquidity_result = json.loads(modify_liquidity.stdout) - assert modify_liquidity_result["success"] is True - assert isinstance(modify_liquidity_result["extrinsic_identifier"], str) - - llr = exec_command_alice( - command="liquidity", - sub_command="list", - extra_args=[ - "--wallet-path", - wallet_path_alice, - "--chain", - "ws://127.0.0.1:9945", - "--wallet-name", - wallet_alice.name, - "--wallet-hotkey", - wallet_alice.hotkey_str, - "--netuid", - netuid, - "--json-output", - ], - ) - print(">>>", llr.stdout, llr.stderr) - liquidity_list_result = json.loads(llr.stdout) - assert len(liquidity_list_result["positions"]) == 1 - liquidity_position = liquidity_list_result["positions"][0] - assert liquidity_position["liquidity"] == 21.0 - - removal = exec_command_alice( - command="liquidity", - sub_command="remove", - extra_args=[ - "--wallet-path", - wallet_path_alice, - "--chain", - "ws://127.0.0.1:9945", - "--wallet-name", - wallet_alice.name, - "--wallet-hotkey", - wallet_alice.hotkey_str, - "--netuid", - netuid, - "--all", - "--no-prompt", - "--json-output", - ], - ) - removal_result = json.loads(removal.stdout) - assert removal_result[str(liquidity_position["id"])]["success"] is True - assert isinstance( - removal_result[str(liquidity_position["id"])]["extrinsic_identifier"], str - ) - - liquidity_list_result = exec_command_alice( - command="liquidity", - sub_command="list", - extra_args=[ - "--wallet-path", - wallet_path_alice, - "--chain", - "ws://127.0.0.1:9945", - "--wallet-name", - wallet_alice.name, - "--wallet-hotkey", - wallet_alice.hotkey_str, - "--netuid", - netuid, - "--json-output", - ], - ) - print(">>>", liquidity_list_result.stdout, liquidity_list_result.stderr) - liquidity_list_result = json.loads(liquidity_list_result.stdout) - assert liquidity_list_result["success"] is False - assert result_output["err_msg"] == "No liquidity positions found." - assert liquidity_list_result["positions"] == [] From 18de42902b37232d8b5acea26de5e895700e1082 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Mon, 8 Jun 2026 18:37:06 +0200 Subject: [PATCH 05/17] Update price fetch from Swap.AlphaSqrtPrice to new runtime call --- .../src/bittensor/subtensor_interface.py | 99 ++++++++++++++++--- 1 file changed, 88 insertions(+), 11 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 9e69ba9a8..0c705b6d8 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -2706,6 +2706,39 @@ async def get_claimable_stakes_for_coldkey( results[hotkey][netuid] = net_claimable.set_unit(netuid) return results + async def _runtime_method_exists( + self, api: str, method: str, block_hash: Optional[str] = None + ) -> bool: + """ + Checks whether a runtime call method exists at the given block. + + :param api: The runtime API name (e.g. `"SwapRuntimeApi"`). + :param method: The method within the runtime API to check for. + :param block_hash: The hash of the block at which to check. + + :return: `True` if the runtime call method exists, `False` otherwise. + """ + runtime = await self.substrate.init_runtime(block_hash=block_hash) + try: + _ = runtime.runtime_api_map[api][method] + return True + except KeyError: + return False + + async def _get_subnet_price_from_storage( + self, netuid: int, block_hash: Optional[str] = None + ) -> Balance: + """Legacy Alpha-price lookup for pre-Balancer chains via ``Swap::AlphaSqrtPrice``.""" + current_sqrt_price = await self.query( + module="Swap", + storage_function="AlphaSqrtPrice", + params=[netuid], + block_hash=block_hash, + ) + current_sqrt_price = fixed_to_float(current_sqrt_price) + current_price = current_sqrt_price * current_sqrt_price + return Balance.from_rao(int(current_price * 1e9)) + async def get_subnet_price( self, netuid: int = None, @@ -2714,22 +2747,34 @@ async def get_subnet_price( """ Gets the current Alpha price in TAO for a specific subnet. + Uses the `SwapRuntimeApi::current_alpha_price` runtime call (Balancer swap). If the connected + chain does not expose that runtime method, falls back to the legacy `Swap::AlphaSqrtPrice` + storage query so the CLI keeps working against pre-Balancer chains. Note this is only necessary to exist + until mainnet release, as it allows for working with the staging branch on live data until then. + :param netuid: The unique identifier of the subnet. :param block_hash: The hash of the block to retrieve the price from. :return: The current Alpha price in TAO units for the specified subnet. """ - # TODO update this to use the runtime call SwapRuntimeAPI.current_alpha_price - current_sqrt_price = await self.query( - module="Swap", - storage_function="AlphaSqrtPrice", - params=[netuid], - block_hash=block_hash, - ) + # SN0 (root) uses TAO directly and always has a price of 1 TAO. + if netuid == 0: + return Balance.from_tao(1) - current_sqrt_price = fixed_to_float(current_sqrt_price) - current_price = current_sqrt_price * current_sqrt_price - return Balance.from_rao(int(current_price * 1e9)) + if await self._runtime_method_exists( + "SwapRuntimeApi", "current_alpha_price", block_hash=block_hash + ): + price_rao = await self.query_runtime_api( + "SwapRuntimeApi", + "current_alpha_price", + params=[netuid], + block_hash=block_hash, + ) + return Balance.from_rao(int(price_rao)) + else: + return await self._get_subnet_price_from_storage( + netuid, block_hash=block_hash + ) async def get_subnet_prices( self, block_hash: Optional[str] = None, page_size: int = 200 @@ -2737,11 +2782,43 @@ async def get_subnet_prices( """ Gets the current Alpha prices in TAO for all subnets. + Prefers the ``SwapRuntimeApi::current_alpha_price_all`` runtime call (Balancer swap), falling + back to per-subnet ``current_alpha_price`` calls, and finally to the legacy + ``Swap::AlphaSqrtPrice`` storage map for pre-Balancer chains. + :param block_hash: The hash of the block to retrieve prices from. - :param page_size: The page size for batch queries (default: 100). + :param page_size: The page size for the legacy storage fallback query. :return: A dictionary mapping netuid to the current Alpha price in TAO units. """ + if block_hash is None: + block_hash = await self.substrate.get_chain_head() + + if await self._runtime_method_exists( + "SwapRuntimeApi", "current_alpha_price_all", block_hash=block_hash + ): + prices_rao = await self.query_runtime_api( + "SwapRuntimeApi", + "current_alpha_price_all", + block_hash=block_hash, + ) + return { + int(p["netuid"]): Balance.from_rao(int(p["price"])) for p in prices_rao + } + + if await self._runtime_method_exists( + "SwapRuntimeApi", "current_alpha_price", block_hash=block_hash + ): + netuids = await self.get_all_subnet_netuids(block_hash=block_hash) + prices_list = await asyncio.gather( + *[ + self.get_subnet_price(netuid, block_hash=block_hash) + for netuid in netuids + ] + ) + return dict(zip(netuids, prices_list)) + + # Legacy fallback for pre-Balancer chains. query = await self.substrate.query_map( module="Swap", storage_function="AlphaSqrtPrice", From 40372e4f26970fa15877a5fa8e696e1d57acd613 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Mon, 8 Jun 2026 18:37:58 +0200 Subject: [PATCH 06/17] Add test --- .../src/bittensor/subtensor_interface.py | 6 +- tests/unit_tests/test_balancer_price.py | 176 ++++++++++++++++++ 2 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 tests/unit_tests/test_balancer_price.py diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 0c705b6d8..dcb90911e 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -2782,9 +2782,9 @@ async def get_subnet_prices( """ Gets the current Alpha prices in TAO for all subnets. - Prefers the ``SwapRuntimeApi::current_alpha_price_all`` runtime call (Balancer swap), falling - back to per-subnet ``current_alpha_price`` calls, and finally to the legacy - ``Swap::AlphaSqrtPrice`` storage map for pre-Balancer chains. + Prefers the `SwapRuntimeApi::current_alpha_price_all` runtime call (Balancer swap), falling + back to per-subnet `current_alpha_price` calls, and finally to the legacy + `Swap::AlphaSqrtPrice` storage map for pre-Balancer chains. :param block_hash: The hash of the block to retrieve prices from. :param page_size: The page size for the legacy storage fallback query. diff --git a/tests/unit_tests/test_balancer_price.py b/tests/unit_tests/test_balancer_price.py new file mode 100644 index 000000000..ed44e838a --- /dev/null +++ b/tests/unit_tests/test_balancer_price.py @@ -0,0 +1,176 @@ +"""Unit tests for the Balancer swap price methods on SubtensorInterface. + +These cover the migration from the old `Swap::AlphaSqrtPrice` storage to the +`SwapRuntimeApi::current_alpha_price` / `current_alpha_price_all` runtime +calls, plus the graceful fallbacks that keep the CLI working against +pre-Balancer chains. +""" + +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from bittensor_cli.src.bittensor.balances import Balance +from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface + + +def _exists_for(*methods): + """Build an async side_effect for _runtime_method_exists that only reports + the given runtime method names as present.""" + + async def _exists(api, method, block_hash=None): + return method in methods + + return _exists + + +@pytest.mark.asyncio +async def test_get_subnet_price_sn0_is_one_tao(): + """SN0 (root) uses TAO directly and is always 1 TAO, no chain calls.""" + subtensor = SubtensorInterface("finney") + with patch.object( + SubtensorInterface, "_runtime_method_exists", new_callable=AsyncMock + ) as exists: + price = await subtensor.get_subnet_price(netuid=0) + assert price == Balance.from_tao(1) + exists.assert_not_called() + + +@pytest.mark.asyncio +async def test_get_subnet_price_uses_runtime_api(): + """When current_alpha_price exists, the runtime call result (rao) is used.""" + subtensor = SubtensorInterface("finney") + with ( + patch.object( + SubtensorInterface, + "_runtime_method_exists", + side_effect=_exists_for("current_alpha_price"), + ), + patch.object( + SubtensorInterface, + "query_runtime_api", + new_callable=AsyncMock, + return_value=2_500_000_000, + ) as query_rt, + ): + price = await subtensor.get_subnet_price(netuid=1, block_hash="0xabc") + + assert price == Balance.from_rao(2_500_000_000) + query_rt.assert_awaited_once_with( + "SwapRuntimeApi", + "current_alpha_price", + params=[1], + block_hash="0xabc", + ) + + +@pytest.mark.asyncio +async def test_get_subnet_price_falls_back_to_storage(): + """On pre-Balancer chains (no runtime method) it falls back to storage.""" + subtensor = SubtensorInterface("finney") + with ( + patch.object( + SubtensorInterface, + "_runtime_method_exists", + side_effect=_exists_for(), # nothing exists + ), + patch.object( + SubtensorInterface, + "_get_subnet_price_from_storage", + new_callable=AsyncMock, + return_value=Balance.from_rao(777), + ) as fallback, + ): + price = await subtensor.get_subnet_price(netuid=3, block_hash="0xabc") + + assert price == Balance.from_rao(777) + fallback.assert_awaited_once_with(3, block_hash="0xabc") + + +@pytest.mark.asyncio +async def test_get_subnet_prices_uses_current_alpha_price_all(): + """Preferred path: a single current_alpha_price_all runtime call.""" + subtensor = SubtensorInterface("finney") + with ( + patch.object( + SubtensorInterface, + "_runtime_method_exists", + side_effect=_exists_for("current_alpha_price_all"), + ), + patch.object( + SubtensorInterface, + "query_runtime_api", + new_callable=AsyncMock, + return_value=[ + {"netuid": 1, "price": 1000}, + {"netuid": 2, "price": 2000}, + ], + ) as query_rt, + ): + prices = await subtensor.get_subnet_prices(block_hash="0xabc") + + assert prices == {1: Balance.from_rao(1000), 2: Balance.from_rao(2000)} + query_rt.assert_awaited_once_with( + "SwapRuntimeApi", "current_alpha_price_all", block_hash="0xabc" + ) + + +@pytest.mark.asyncio +async def test_get_subnet_prices_per_netuid_fallback(): + """If only current_alpha_price exists, prices are fetched per-netuid.""" + subtensor = SubtensorInterface("finney") + + async def fake_price(netuid, block_hash=None): + return Balance.from_rao(netuid * 100) + + with ( + patch.object( + SubtensorInterface, + "_runtime_method_exists", + side_effect=_exists_for("current_alpha_price"), + ), + patch.object( + SubtensorInterface, + "get_all_subnet_netuids", + new_callable=AsyncMock, + return_value=[0, 1, 2], + ), + patch.object(SubtensorInterface, "get_subnet_price", side_effect=fake_price), + ): + prices = await subtensor.get_subnet_prices(block_hash="0xabc") + + assert prices == { + 0: Balance.from_rao(0), + 1: Balance.from_rao(100), + 2: Balance.from_rao(200), + } + + +@pytest.mark.asyncio +async def test_get_subnet_prices_legacy_storage_fallback(): + """On pre-Balancer chains, fall back to the AlphaSqrtPrice storage map.""" + subtensor = SubtensorInterface("finney") + subtensor.substrate = MagicMock() + # raw sqrt-price values; fixed_to_float is patched to identity below + subtensor.substrate.query_map = AsyncMock( + return_value=MagicMock(records=[(1, 1.0), (2, 2.0)]) + ) + + with ( + patch.object( + SubtensorInterface, + "_runtime_method_exists", + side_effect=_exists_for(), # nothing exists + ), + patch( + "bittensor_cli.src.bittensor.subtensor_interface.fixed_to_float", + side_effect=lambda v: float(v), + ), + ): + prices = await subtensor.get_subnet_prices(block_hash="0xabc") + + # price = sqrt**2 * 1e9 rao + assert prices == { + 1: Balance.from_rao(int((1.0**2) * 1e9)), + 2: Balance.from_rao(int((2.0**2) * 1e9)), + } From 09b6d5aba2ecb21f2633513e250a10e2cbdf85ef Mon Sep 17 00:00:00 2001 From: BD Himes Date: Mon, 8 Jun 2026 19:00:41 +0200 Subject: [PATCH 07/17] Test fix --- bittensor_cli/src/commands/subnets/subnets.py | 2 +- tests/e2e_tests/test_wallet_interactions.py | 5 +---- tests/e2e_tests/utils.py | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index a69d3aea9..16ebc4f79 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -496,7 +496,7 @@ def define_table( ) defined_table.add_column( f"[bold white]Stake ({Balance.get_unit(1)}_out)", - style=COLOR.STAKE_ALPHA, + style=COLOR.S.ALPHA, justify="left", ) defined_table.add_column( diff --git a/tests/e2e_tests/test_wallet_interactions.py b/tests/e2e_tests/test_wallet_interactions.py index 02797b978..f271699cf 100644 --- a/tests/e2e_tests/test_wallet_interactions.py +++ b/tests/e2e_tests/test_wallet_interactions.py @@ -85,10 +85,7 @@ def test_wallet_overview_inspect(local_chain, wallet_setup): subnets_list = exec_command( command="subnets", sub_command="list", - extra_args=[ - "--chain", - "ws://127.0.0.1:9945", - ], + extra_args=["--chain", "ws://127.0.0.1:9945"], ) sleep(3) diff --git a/tests/e2e_tests/utils.py b/tests/e2e_tests/utils.py index 9c77f2770..5a8e368f7 100644 --- a/tests/e2e_tests/utils.py +++ b/tests/e2e_tests/utils.py @@ -156,7 +156,7 @@ def find_stake_entries( return matching_stakes -def verify_subnet_entry(output_text: str, netuid: str, ss58_address: str) -> bool: +def verify_subnet_entry(output_text: str, netuid: str | int, ss58_address: str) -> bool: """ Verifies the presence of a specific subnet entry subnets list output. From 1481012d61bfa50c26f74fca37dc12b2405aaf17 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Mon, 8 Jun 2026 19:20:59 +0200 Subject: [PATCH 08/17] Test fix --- tests/e2e_tests/test_staking_sudo.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index 68633b3dd..c12486ed0 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -296,8 +296,14 @@ def test_staking(local_chain, wallet_setup): 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 + # Under the Balancer swap, a registered-but-not-yet-started subnet is still + # non-dynamic, so the chain's current_alpha_price returns 1.0 (1:1 with TAO). + # (Pre-Balancer this read empty AlphaSqrtPrice storage and was 0.0.) + assert stats["current_price"] == 1.0 + # market_cap = price * (alpha_in + alpha_out) = supply, since price is 1.0. + # Pre-Balancer this was 0.0 because price was 0.0; now it surfaces the supply. + assert stats["market_cap"] == stats["supply"] + assert stats["market_cap"] == 1000.0 # Start emissions on SNs for netuid_ in multiple_netuids: From dc0470ef0d74985498fafc724857284e58ec2132 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Wed, 10 Jun 2026 17:28:06 +0200 Subject: [PATCH 09/17] Removes old CONTRIBUTING file --- CONTRIBUTING.md | 332 ------------------------------------------------ 1 file changed, 332 deletions(-) delete mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 8db2c170a..000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,332 +0,0 @@ -# Contributing to BTCLI (Bittensor CLI) - -The following is a set of guidelines for contributing to btcli, which is hosted in the [Opentensor Organization](https://github.com/opentensor) on GitHub. -These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. - -## Table Of Contents -1. [I don't want to read this whole thing, I just have a question!!!](#i-dont-want-to-read-this-whole-thing-i-just-have-a-question) -2. [What should I know before I get started?](#what-should-i-know-before-i-get-started) -3. [Getting Started](#getting-started) - 1. [Good First Issue Label](#good-first-issue-label) - 2. [Beginner and Help-wanted Issues Label](#beginner-and-help-wanted-issues-label) -4. [How Can I Contribute?](#how-can-i-contribute) - 1. [Code Contribution General Guideline](#code-contribution-general-guidelines) - 2. [Pull Request Philosophy](#pull-request-philosophy) - 3. [Pull Request Process](#pull-request-process) - 4. [Testing](#testing) - 5. [Addressing Feedback](#addressing-feedback) - 6. [Squashing Commits](#squashing-commits) - 7. [Refactoring](#refactoring) - 8. [Peer Review](#peer-review) -5. [Reporting Bugs](#reporting-bugs) -6. [Suggesting Features](#suggesting-enhancements-and-features) - - -## I don't want to read this whole thing I just have a question! - -> **Note:** Please don't file an issue to ask a question. You'll get faster results by using the resources below. - -We have an official Discord server where the community chimes in with helpful advice if you have questions. -This is the fastest way to get an answer and the core development team is active on Discord. Also linked is the -more community-oriented Church of Rao Discord, which has channels that focus more on development than specific subnets -or generalities. - -* [Official Bittensor Discord](https://discord.gg/bittensor) -* [Church of Rao Discord](https://discord.gg/brRAeVCmzM) - -## What should I know before I get started? -Bittensor is constantly growing with new features, and as such you will potentially run into some problems running btcli. -If you run into an issue or end up resolving an issue yourself, -feel free to create a pull request with a fix or with a fix to the documentation. The documentation repository -can be found [here](https://github.com/latent-to/developer-docs). - -Additionally, note that the core implementation of Bittensor consists of three separate repositories: -[The core Bittensor code](https://github.com/opentensor/bittensor), the [btcli](https://github.com/opentensor/btcli), -and the Bittensor Blockchain [subtensor](https://github.com/opentensor/subtensor). - -See the [Tao.app](https://www.tao.app/explorer) explorer for a list of all the repositories for the active registered subnets. - -## Getting Started -New contributors are very welcome and needed. -Reviewing and testing is highly valued and the most effective way you can contribute as a new contributor. -It also will teach you much more about the code and process than opening pull requests. - -There are frequently open issues of varying difficulty waiting to be fixed. -If you're looking for somewhere to start contributing, check out the [good first issue](https://github.com/opentensor/btcli/labels/good%20first%20issue) -list or changes that are up for grabs. Some of them might no longer be applicable. -So if you are interested, but unsure, you might want to leave a comment on the issue first. -Also peruse the [issues](https://github.com/opentensor/btcli/issues) tab for all open issues. - -### Good First Issue Label -The purpose of the good first issue label is to highlight which issues are suitable for a new contributor without a deep understanding of the codebase. - -However, good first issues can be solved by anyone. If they remain unsolved for a longer time, a frequent contributor might address them. - -You do not need to request permission to start working on an issue. However, you are encouraged to leave a comment -if you are planning to work on it. This will help other contributors monitor which issues are actively being -addressed and is also an effective way to request assistance if and when you need it. - -### Beginner and Help-wanted Issues Label -You can start by looking through these `beginner` and `help-wanted` issues: - -* [Beginner issues](https://github.com/opentensor/btcli/labels/beginner) - issues which should only require a few lines of code, and a test or two. -* [Help wanted issues](https://github.com/opentensor/btcli/labels/help%20wanted) - issues which should be a bit more involved than `beginner` issues. - -## Communication Channels -Most communication about Bittensor/btcli development happens on [Discord](https://discord.gg/bittensor). - -You can engage with the community in the [general](https://discord.com/channels/799672011265015819/799672011814862902) channel and follow the release announcements posted [here](https://discord.com/channels/799672011265015819/1359587876563718144). - -## How Can I Contribute? - -You can contribute to btcli in one of two main ways (as well as many others): -1. [Bug](#reporting-bugs) reporting and fixes -2. New features [enhancements](#suggesting-enhancements-and-features) - -### Style Guide -Here is a high-level summary of the bittensor style guide: -- Code consistency is crucial; adhere to established programming language conventions. -- Use `ruff format .` to format your Python code; it ensures readability and consistency. - - Verify you are using the same version of `ruff` as is declared in the [pyproject.toml](./pyproject.toml) -- Write concise Git commit messages; summarize changes in ~50 characters. -- Follow these six commit rules: - - Atomic Commits: Focus on one task or fix per commit. - - Subject and Body Separation: Use a blank line to separate the subject from the body. - - Subject Line Length: Keep it under 50 characters for readability. - - Imperative Mood: Write subject line as if giving a command or instruction. - - Body Text Width: Wrap text manually at 72 characters. - - Body Content: Explain what changed and why, not how. -- Make use of your commit messages to simplify project understanding and maintenance. - -### Code Contribution General Guidelines - -If you're looking to contribute to btcli but unsure where to start, -please join our community [discord](https://discord.gg/bittensor), a developer-friendly Bittensor town square. -You can also browse through the GitHub [issues](https://github.com/opentensor/btcli/issues) to see where help might be needed. -For a greater understanding of btcli's usage and development, check the [Bittensor Documentation](https://docs.learnbittensor.org). - -All PRs must be opened against the `staging` branch. Use appropriate labels and provide an in-depth description of what your PR does, -what changes it makes, which issues it resolves, etc. - -#### Pull Request Philosophy - -Patchsets and enhancements should always be focused. A pull request could add a feature, fix a bug, or refactor code, -but it should not contain a mixture of these. Please also avoid 'super' pull requests which attempt to do too much, -are overly large, or overly complex as this makes review difficult. - -Specifically, pull requests **must** adhere to the following criteria: -- **Must** branch off from `staging`. Make sure that all your PRs are using `staging` branch as a base or they **will** be closed. -- Contain a reasonable number of changes for the stated purpose of the PR. PRs that contain an excessive number of lines of code or files without a valid rationale may be closed. -- If a PR introduces a new feature, it **must** include corresponding tests. -- Other PRs (bug fixes, refactoring, etc.) should ideally also have tests, as they provide proof of concept and prevent regression. -- Categorize your PR properly by using GitHub labels. This aids in the review process by informing reviewers about the type of change at a glance. -- Make sure your code includes adequate, but not unnecessary comments. These should explain why certain decisions were made and how your changes work. -- If your changes are extensive, consider breaking your PR into smaller, related PRs. This makes your contributions easier to understand and review. -- Be active in the discussion about your PR. Respond promptly to comments and questions to help reviewers understand your changes and speed up the acceptance process. - -Generally, all pull requests must: - - - Have a clear use case, fix a demonstrable bug or serve the greater good of the project (e.g. refactoring for modularisation). - - Be well peer-reviewed. - - Follow code style guidelines. - - Not break the existing test suite. - - Where bugs are fixed, where possible, there should be unit tests demonstrating the bug and also proving the fix. - - Change relevant comments and documentation when behaviour of code changes. - -#### Pull Request Process - -Please follow these steps to have your contribution considered by the maintainers: - -*Before* creating the PR: -1. Ensure your PR meets the criteria stated in the [Pull Request Philosophy](#pull-request-philosophy) section. -2. Include relevant tests for any fixed bugs or new features as stated in the [testing guide](./TESTING.md). -3. Ensure your commit messages are clear and concise. Include the issue number if applicable. -4. Explain what your changes do and why you think they should be merged in the PR description -5. Ensure your code is formatted correctly with [`ruff`](#style-guide) -6. Ensure [all tests pass](#testing): run `pytest tests/` - -*After* creating the PR: -1. Verify that all [status checks](https://help.github.com/articles/about-status-checks/) are passing after you submit your pull request. -2. Label your PR using GitHub's labeling feature. The labels help categorize the PR and streamline the review process. - -Please be responsive and participate in the discussion on your PR! This aids in clarifying any confusion or concerns and -leads to quicker resolution and merging of your PR. - -> Note: If your changes are not ready for merge but you want feedback, create a draft pull request. - -Following these criteria will aid in quicker review and potential merging of your PR. -While the prerequisites above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted. - -When you are ready to submit your changes, create a pull request. - -After you submit a pull request, it will be reviewed by the maintainers. -They may ask you to make changes. Please respond to any comments and push your changes as a new commit. - -> Note: Be sure to merge the latest from "upstream" before making a pull request: - -```bash -git remote add upstream https://github.com/opentensor/btcli.git -git fetch upstream -git merge upstream/ -git push origin -``` - -#### Testing -Before making a PR for any code changes, please write adequate testing with pytest if it is warranted. -This is **mandatory** for new features and enhancements. - -You may also like to view the [/tests](https://github.com/opentensor/btcli/tree/master/tests) for starter examples. - -Here is a quick summary: -- **Running Tests**: Use `pytest` from the root directory of the btcli repository to run all tests. To run a specific test file or a specific test within a file, specify it directly (e.g., `pytest tests/e2e_tests/test_wallet_interactions.py::test_wallet_overview_inspect`). - - Before submitting your PR, ensure that all tests pass by running `pytest tests/` -- **Writing Tests**: When writing tests, cover both the "happy path" and any potential error conditions. Use the `assert` statement to verify the expected behavior of a function. -- **Mocking**: Use the `pytest` library to mock certain functions (with monkeypatch) or objects when you need to isolate the functionality you're testing. This allows you to control the behavior of these functions or objects during testing. -- **Test Coverage**: Use the `pytest-cov` plugin to measure your test coverage. Aim for high coverage but also ensure your tests are meaningful and accurately represent the conditions under which your code will run. -- **Continuous Integration**: btcli uses GitHub Actions for continuous integration. Tests are automatically run every time you push changes to the repository. Check the "Actions" tab of the btcli GitHub repository to view the results. - -Remember, testing is crucial for maintaining code health, catching issues early, and facilitating the addition of new features or refactoring of existing code. - -#### Addressing Feedback - -After submitting your pull request, expect comments and reviews from other contributors. -You can add more commits to your pull request by committing them locally and pushing to your fork. - -You are expected to reply to any review comments before your pull request is merged. -You may update the code or reject the feedback if you do not agree with it, but you should express so in a reply. -If there is outstanding feedback and you are not actively working on it, your pull request may be closed. - -#### Squashing Commits - -If your pull request contains fixup commits (commits that change the same line of code repeatedly) or too fine-grained commits, you may be asked to [squash](https://git-scm.com/docs/git-rebase#_interactive_mode) your commits before it will be reviewed. The basic squashing workflow is shown below. - - git checkout your_branch_name - git rebase -i HEAD~n - # n is normally the number of commits in the pull request. - # Set commits (except the one in the first line) from 'pick' to 'squash', save and quit. - # On the next screen, edit/refine commit messages. - # Save and quit. - git push -f # (force push to GitHub) - -Please update the resulting commit message, if needed. It should read as a coherent message. In most cases, -this means not just listing the interim commits. - -If your change contains a merge commit, the above workflow may not work and you will need to remove the merge -commit first. See the next section for details on how to rebase. - -Please refrain from creating several pull requests for the same change. Use the pull request that is already open -(or was created earlier) to amend changes. This preserves the discussion and review that happened earlier for the respective change set. - -The length of time required for peer review is unpredictable and will vary from pull request to pull request. - -#### Refactoring - -Refactoring is a necessary part of any software project's evolution. -The following guidelines cover refactoring pull requests for the btcli project. - -There are three categories of refactoring: code-only moves, code style fixes, and code refactoring. In general, -refactoring pull requests should not mix these three kinds of activities in order to make refactoring pull requests -easy to review and uncontroversial. In all cases, refactoring PRs must not change the behaviour of code within the -pull request (bugs must be preserved as is). - -Project maintainers aim for a quick turnaround on refactoring pull requests, so where possible keep them short, -uncomplex and easy to verify. - -Pull requests that refactor the code should not be made by new contributors. -It requires a certain level of experience to know where the code belongs to and to understand the full -ramification (including rebase effort of open pull requests). Trivial pull requests or pull requests that -refactor the code with no clear benefits may be immediately closed by the maintainers to reduce -unnecessary workload on reviewing. - -#### Peer Review - -Anyone may participate in peer review which is expressed by comments in the pull request. -Typically, reviewers will review the code for obvious errors, as well as test out the patch set and -opine on the technical merits of the patch. Project maintainers take into account the peer review when -determining if there is consensus to merge a pull request (remember that discussions may have taken -place elsewhere, not just on GitHub). - -A pull request that changes consensus-critical code is considerably more involved than a pull request that adds a -feature to the logger output, for example. Such patches must be reviewed and thoroughly tested by several -reviewers who are knowledgeable about the changed subsystems. Where new features are proposed, -it is helpful for reviewers to try out the patch set on a test network and indicate that they have done -so in their review. Project maintainers will take this into consideration when merging changes. - -### Reporting Bugs - -This section guides you through submitting a bug report for btcli. -Following these guidelines helps maintainers and the community understand your report, reproduce the behavior, and find -related reports. - -When you are creating a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report). - -> **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, -> open a new issue and include a link to the original issue in the body of your new one. - -#### Before Submitting A Bug Report - -* **Check the [Discord Server](https://discord.gg/bittensor)** and ask in [#general](https://discord.com/channels/799672011265015819/799672011814862902). -* **Determine which repository the problem should be reported in**: if it has to do with incorrect client-side behavior, -then it's likely [btcli](https://github.com/opentensor/btcli). -If you are having problems with your emissions or Blockchain, then it is in [subtensor](https://github.com/opentensor/subtensor). - -#### How Do I Submit A (Good) Bug Report? - -Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/). You can find btcli's issues [here](https://github.com/opentensor/btcli/issues). -After you've determined which repository ([btcli](https://github.com/opentensor/btcli) or [subtensor](https://github.com/opentensor/subtensor)) your bug is related to, create an issue on that repository. - -Explain the problem and include additional details to help maintainers reproduce the problem: - -* **Use a clear and descriptive title** for the issue to identify the problem. -* **Describe the exact steps which reproduce the problem** in as many details as possible. -For example, start by explaining how you started btcli, e.g. which command exactly you used in the terminal, -When listing steps, **don't just say what you did, but explain how you did it**. -* **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/pasteable snippets, -which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://docs.github.com/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks). -* **Explain which behavior you expected to see instead and why.** -* **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. -On macOS, the crash report will be available in `Console.app` under "Diagnostic and usage information" > "User diagnostic reports". -Include the crash report in the issue in a [code block](https://docs.github.com/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks), a [file attachment](https://docs.github.com/articles/file-attachments-on-issues-and-pull-requests/), or put it in a [gist](https://gist.github.com/) and provide link to that gist. -* **If the problem wasn't triggered by a specific action**, describe what you were doing before the problem happened and share more information using the guidelines below. -* **Use `--debug`**, by running `btcli --debug` after a failing command, a debug report will be generated that removes all sensitive information. Include this debug report in your Issue. - -Provide more context by answering these questions: - -* **Did the problem start happening recently** (e.g. after updating to a new version of btcli) or was this always a problem? -* If the problem started happening recently, **can you reproduce the problem in an older version of btcli?** -* **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens. - -Include details about your configuration and environment: - -* **Which version of btcli are you using?** You can get the version of btcli by executing the `btcli --version` command. -* **What's the name and version of the OS you're using**? -* **Are you running btcli in a virtual machine?** If so, which VM software are you using and which operating systems and versions are used for the host and the guest? - -### Suggesting Enhancements and Features - -This section guides you through submitting an enhancement suggestion for btcli, -including completely new features and minor improvements to existing functionality. Following these guidelines helps -maintainers and the community understand your suggestion and find related suggestions. - -When you are creating an enhancement suggestion, please include as many details as possible. - -#### Before Submitting An Enhancement Suggestion - -* **Determine which repository the problem should be reported in**: if it has to do with unexpected client-side behavior, then it's likely [btcli](https://github.com/opentensor/btcli). -If you are having problems with your emissions or Blockchain, then it is in [subtensor](https://github.com/opentensor/subtensor) - -#### How To Submit A (Good) Feature Suggestion - -Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/). After you've determined which repository ([btcli](https://github.com/opentensor/btcli) or [subtensor](https://github.com/opentensor/subtensor)) your enhancement suggestion is related to, create an issue on that repository and provide the following information: - -* **Use a clear and descriptive title** for the issue to identify the problem. -* **Provide a step-by-step description of the suggested enhancement** in as many details as possible. -* **Provide specific examples to demonstrate the steps**. Include copy/pasteable snippets which you use in those examples, as [Markdown code blocks](https://docs.github.com/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks). -* **Describe the current behavior** and **explain which behavior you expected to see instead** and why. -* **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. -* **Explain why this enhancement would be useful** to most btcli users. -* **List some other text editors or applications where this enhancement exists.** -* **Specify which version of btcli are you using?** You can get the version of the btcli by executing the `btcli --version` command. -* **Specify the name and version of the OS you're using.** - -Thank you for considering contributing to btcli! Any help is greatly appreciated along this journey to incentivize open and permissionless intelligence. From 13364177a9860c94a104457e9704797f5cd9ca8e Mon Sep 17 00:00:00 2001 From: BD Himes Date: Wed, 10 Jun 2026 17:28:25 +0200 Subject: [PATCH 10/17] Adds info about squashing and signing commits --- contrib/CONTRIBUTING.MD | 50 ++++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/contrib/CONTRIBUTING.MD b/contrib/CONTRIBUTING.MD index fe1d98098..707b45aab 100644 --- a/contrib/CONTRIBUTING.MD +++ b/contrib/CONTRIBUTING.MD @@ -1,18 +1,21 @@ # Contributing to Bittensor CLI + We want to make contributing to this project as easy and transparent as possible. -We have an official Discord server where the community chimes in with helpful advice if you have questions. +We have an official Discord server where the community chimes in with helpful advice if you have questions. This is the fastest way to get an answer and the core development team is active on Discord. * [Official Bittensor Discord](https://discord.gg/7wvFuPJZgq) * [Bittensor Developers Group (Church of Rao)](https://discord.gg/4AMrc7B4) ## Issues + We use GitHub issues to track bugs and improvements. Please ensure your description is clear and has sufficient instructions to be able to reproduce the issue. When submitting a bug report, please include the following information: + 1. Python version 2. Network or Chain which was being used to interact with Subtensor 3. Steps to reproduce the issue @@ -20,6 +23,7 @@ When submitting a bug report, please include the following information: 5. Any error messages or stack traces encountered ## Pull Requests + We welcome your pull requests. To ensure a smooth and efficient review process, please follow these guidelines: ### Bug fixes & Improvements: @@ -32,18 +36,22 @@ We welcome your pull requests. To ensure a smooth and efficient review process, 6. Provide detailed explanation what your PR fixes, alternate designs, and possible ripple effects. ### Feature Requests -We welcome feature requests and suggestions for improving the Bittensor CLI. To submit a feature request, please follow these guidelines: -1. If your feature request doesn't already exist, create a new issue on GitHub with the label "enhancement" or "feature request". +We welcome feature requests and suggestions for improving the Bittensor CLI. To submit a feature request, please follow +these guidelines: + +1. If your feature request doesn't already exist, create a new issue on GitHub with the label "enhancement" or "feature + request". 3. Provide a clear and descriptive title for the feature request. -4. Explain the feature you'd like to see added, why it would be useful, and how it could be implemented. Be as specific as possible. +4. Explain the feature you'd like to see added, why it would be useful, and how it could be implemented. Be as specific + as possible. 5. If applicable, include examples, screenshots, or mockups to help illustrate your feature request. -6. Be patient and understanding. The maintainers will review your feature request and provide feedback. - +6. Be patient and understanding. The maintainers will review your feature request and provide feedback. ### Signed Commits -All commits in pull requests must be signed. We require signed commits to verify the authenticity of contributions and ensure code integrity. +All commits in pull requests must be signed. We require signed commits to verify the authenticity of contributions and +ensure code integrity. To sign your commits, you must have GPG signing configured in Git: @@ -57,11 +65,33 @@ Or configure Git to sign all commits automatically: git config --global commit.gpgsign true ``` -For instructions on setting up GPG key signing, see [GitHub's documentation on signing commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits). +For instructions on setting up GPG key signing, +see [GitHub's documentation on signing commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits). > **Note:** Pull requests containing unsigned commits will not be merged. +#### Oeps, I forgot to sign! + +Let's say you've opened a PR and have added a commit that is unsigned. It's simple to squash the commits in the branch, +and then sign that single commit, pushing to your PR branch: + +```shell +# 1. Make sure your local staging is up to date +git fetch origin + +# 2. Soft-reset to the point where your branch diverged from staging +# (keeps all your changes staged, discards the commits themselves) +git reset --soft $(git merge-base origin/staging HEAD) + +# 3. Create one new signed commit with everything +git commit -S -m "Your squashed commit message" + +# 4. Force-push to update the PR +git push --force-with-lease +``` + ### Tests -Try to cover with unit/e2e tests any changes you're making. -Make strong use of the `conftest.py` file (e.g. do not recreate test fixtures unless they're not there). + +Try to cover with unit/e2e tests any changes you're making. +Make strong use of the `conftest.py` file (e.g. do not recreate test fixtures unless they're not there). If you need a test fixture that is not there, add it in a reusable way to `conftest.py`. From 4b5a22ca84245be4724fbd9f20ba33d857fb0e08 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Wed, 10 Jun 2026 18:41:18 +0200 Subject: [PATCH 11/17] Updates pr guard to pass instead of skip, and use correct auth for comment posting --- .github/workflows/pr-guard.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-guard.yml b/.github/workflows/pr-guard.yml index e2e4e2267..1693e2ad5 100644 --- a/.github/workflows/pr-guard.yml +++ b/.github/workflows/pr-guard.yml @@ -5,15 +5,15 @@ permissions: pull-requests: write on: - pull_request: + pull_request_target: types: [ opened, edited, synchronize, reopened ] jobs: target-branch: - if: github.base_ref == 'main' && !startsWith(github.head_ref, 'release') runs-on: ubuntu-latest steps: - name: Comment and fail when targeting main from a non-release branch + if: github.base_ref == 'main' && !startsWith(github.head_ref, 'release') uses: actions/github-script@v9 with: script: | @@ -23,7 +23,7 @@ jobs: issue_number: context.issue.number, body: 'PRs need to be open against staging.', }); - core.setFailed('PRs need to be open against the 'staging' branch.'); + core.setFailed("PRs need to be open against the 'staging' branch."); signed-commits: runs-on: ubuntu-latest From 57add5141b947bd9a1cc4d83d9f86f7797e431f3 Mon Sep 17 00:00:00 2001 From: kilyanni Date: Wed, 10 Jun 2026 20:17:13 +0200 Subject: [PATCH 12/17] fix(pyproject): drop unused wheel dep --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 78519a8ef..2fa34adb4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,6 @@ classifiers = [ "Topic :: Utilities" ] dependencies = [ - "wheel>0.46.1", "async-substrate-interface>=2.0.4,<3.0.0", "aiohttp~=3.13", "bittensor-drand>=1.3.0", From 0922db287b11e01e81d202ffb123a59360bd0bfb Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 11 Jun 2026 10:50:00 +0200 Subject: [PATCH 13/17] Updates for cyscale 0.5.0 --- bittensor_cli/src/bittensor/subtensor_interface.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index dcb90911e..6248cbc3f 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -48,7 +48,7 @@ ProxyAnnouncements, ) from scalecodec.base import ScaleType -from scalecodec.utils.math import fixed_to_decimal +from scalecodec.utils.math import fixed_to_decimal, FixedPoint GENESIS_ADDRESS = "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" @@ -2523,7 +2523,7 @@ async def get_claimable_rate_all_netuids( Returns: dict[int, float]: Dictionary mapping netuid to claimable rate. """ - query = await self.query( + query: dict[int, FixedPoint] = await self.query( module="SubtensorModule", storage_function="RootClaimable", params=[hotkey_ss58], @@ -2533,8 +2533,10 @@ async def get_claimable_rate_all_netuids( if not query: return {} - bits_list = next(iter(query)) - return {bits[0]: fixed_to_float(bits[1], frac_bits=32) for bits in bits_list} + return { + netuid: fixed_to_float(bits, frac_bits=32) + for (netuid, bits) in query.items() + } async def get_claimable_rate_netuid( self, From b0d8a19470454cdf9d582db30bcc309375c214ca Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 11 Jun 2026 10:50:21 +0200 Subject: [PATCH 14/17] Pins cyscale to 0.5.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 78519a8ef..842ea2d40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ dependencies = [ "Jinja2", "PyYAML~=6.0", "rich>=15.0,<16.0", - "cyscale>=0.3.3,<1.0.0", + "cyscale==0.5.0", "typer~=0.26.0", "typing_extensions>4.0.0; python_version<'3.11'", "bittensor-wallet==4.1.0", From 017e90c35269417ceae3e470dc2a641d0f12a856 Mon Sep 17 00:00:00 2001 From: wouter Date: Thu, 11 Jun 2026 13:25:23 +0200 Subject: [PATCH 15/17] feat(sudo): add min_childkey_take subnet hyperparameter Register min_childkey_take in HYPERPARAMS and use the generic sudo set path, matching other u16 subnet hyperparameters like burn_half_life. Subtensor #2660 --- bittensor_cli/src/__init__.py | 10 ++++ bittensor_cli/src/bittensor/utils.py | 1 + tests/unit_tests/test_hyperparams.py | 18 ++++++ .../unit_tests/test_sudo_min_childkey_take.py | 55 +++++++++++++++++++ 4 files changed, 84 insertions(+) create mode 100644 tests/unit_tests/test_sudo_min_childkey_take.py diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 779318dd5..3d3b919b3 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -639,6 +639,10 @@ class RootSudoOnly(Enum): "max_allowed_uids": ("sudo_set_max_allowed_uids", RootSudoOnly.FALSE), "burn_increase_mult": ("sudo_set_burn_increase_mult", RootSudoOnly.FALSE), "burn_half_life": ("sudo_set_burn_half_life", RootSudoOnly.FALSE), + "min_childkey_take": ( + "sudo_set_min_childkey_take_per_subnet", + RootSudoOnly.FALSE, + ), } # Maps a hyperparameter to a non-default pallet for sudo set calls. Empty by default @@ -870,6 +874,12 @@ class RootSudoOnly(Enum): "owner_settable": True, "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#burnincreasemult", }, + "min_childkey_take": { + "description": "Minimum childkey take (%) required on this subnet. Settable by the subnet owner. Cannot be set below the global protocol minimum.", + "side_effects": "Child hotkeys on this subnet must set take at or above this value.", + "owner_settable": True, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#minchildkeytake", + }, } # Help Panels for cli help diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 3b5ecd19a..a10843f40 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -852,6 +852,7 @@ def normalize_hyperparameters( "alpha_high": u16_normalized_float, "alpha_low": u16_normalized_float, "alpha_sigmoid_steepness": u16_normalized_float, + "min_childkey_take": u16_normalized_float, "min_burn": Balance.from_rao, "max_burn": Balance.from_rao, } diff --git a/tests/unit_tests/test_hyperparams.py b/tests/unit_tests/test_hyperparams.py index 43f23a599..970ff410b 100644 --- a/tests/unit_tests/test_hyperparams.py +++ b/tests/unit_tests/test_hyperparams.py @@ -9,6 +9,8 @@ "recycle_or_burn", } +MIN_CHILDKEY_TAKE = "min_childkey_take" + def test_new_hyperparams_in_hyperparams(): for key in NEW_HYPERPARAMS_826: @@ -47,3 +49,19 @@ def test_max_burn_is_owner_or_root_settable(): def test_max_burn_metadata_owner_settable_true(): assert HYPERPARAMS_METADATA["max_burn"]["owner_settable"] is True + + +def test_min_childkey_take_in_hyperparams(): + extrinsic, root_only = HYPERPARAMS[MIN_CHILDKEY_TAKE] + assert extrinsic == "sudo_set_min_childkey_take_per_subnet" + assert root_only is RootSudoOnly.FALSE + + +def test_min_childkey_take_has_metadata(): + required = {"description", "side_effects", "owner_settable", "docs_link"} + assert MIN_CHILDKEY_TAKE in HYPERPARAMS_METADATA + meta = HYPERPARAMS_METADATA[MIN_CHILDKEY_TAKE] + for field in required: + assert field in meta, f"{MIN_CHILDKEY_TAKE} metadata missing '{field}'" + assert meta["owner_settable"] is True + assert "#minchildkeytake" in meta["docs_link"] diff --git a/tests/unit_tests/test_sudo_min_childkey_take.py b/tests/unit_tests/test_sudo_min_childkey_take.py new file mode 100644 index 000000000..3f44e73f5 --- /dev/null +++ b/tests/unit_tests/test_sudo_min_childkey_take.py @@ -0,0 +1,55 @@ +import pytest +from unittest.mock import AsyncMock, MagicMock, patch + +from bittensor_cli.src.bittensor.utils import float_to_u16 + +from .conftest import COLDKEY_SS58 + +MODULE = "bittensor_cli.src.commands.sudo" + + +@pytest.mark.asyncio +async def test_min_childkey_take_owner_composes_extrinsic( + mock_wallet, mock_subtensor, successful_receipt +): + from bittensor_cli.src.commands.sudo import set_hyperparameter_extrinsic + + take_u16 = float_to_u16(0.06) + direct_call = MagicMock(name="direct_call") + mock_subtensor.query = AsyncMock(return_value=COLDKEY_SS58) + mock_subtensor.substrate.metadata = MagicMock() + mock_subtensor.substrate.get_metadata_call_function = AsyncMock( + return_value={"fields": [{"name": "netuid"}, {"name": "take"}]} + ) + mock_subtensor.substrate.compose_call = AsyncMock(return_value=direct_call) + mock_subtensor.sign_and_send_extrinsic = AsyncMock( + return_value=(True, "", successful_receipt) + ) + + with ( + patch(f"{MODULE}.unlock_key", return_value=MagicMock(success=True)), + patch(f"{MODULE}.requires_bool", return_value=False), + patch(f"{MODULE}.print_extrinsic_id", new_callable=AsyncMock), + ): + success, err_msg, ext_id = await set_hyperparameter_extrinsic( + subtensor=mock_subtensor, + wallet=mock_wallet, + netuid=18, + proxy=None, + parameter="min_childkey_take", + value=take_u16, + wait_for_inclusion=False, + wait_for_finalization=False, + prompt=False, + normalize=False, + ) + + assert success is True + assert err_msg == "" + assert ext_id == "0x123-1" + mock_subtensor.substrate.compose_call.assert_awaited_once_with( + call_module="AdminUtils", + call_function="sudo_set_min_childkey_take_per_subnet", + call_params={"netuid": 18, "take": take_u16}, + block_hash=mock_subtensor.substrate.last_block_hash, + ) From e07cce9a8ae0ad6c5c0d1c98304eed64b5cab2e3 Mon Sep 17 00:00:00 2001 From: wouter Date: Thu, 11 Jun 2026 14:13:14 +0200 Subject: [PATCH 16/17] feat(wallet): add --coldkeys-only and natural sort to wallet list Add --coldkeys-only to skip hotkey enumeration for large wallet dirs. Sort coldkeys and hotkeys naturally by name (item2 before item10). --- bittensor_cli/cli.py | 10 +- bittensor_cli/src/commands/wallets.py | 32 +++++- tests/unit_tests/test_wallet_list.py | 145 ++++++++++++++++++++++++++ 3 files changed, 183 insertions(+), 4 deletions(-) create mode 100644 tests/unit_tests/test_wallet_list.py diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index dfa7caf97..291032286 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2586,6 +2586,11 @@ def wallet_list( self, wallet_name: Optional[str] = Options.wallet_name, wallet_path: str = Options.wallet_path, + coldkeys_only: bool = typer.Option( + False, + "--coldkeys-only", + help="List coldkeys only; omit hotkeys from the output.", + ), quiet: bool = Options.quiet, verbose: bool = Options.verbose, json_output: bool = Options.json_output, @@ -2596,11 +2601,13 @@ def wallet_list( The output display shows each wallet and its associated `ss58` addresses for the coldkey public key and any hotkeys. The output is presented in a hierarchical tree format, with each wallet as a root node and any associated hotkeys as child nodes. The `ss58` address (or an `` marker, for encrypted hotkeys) is displayed for each coldkey and hotkey that exists on the device. Upon invocation, the command scans the wallet directory and prints a list of all the wallets, indicating whether the - public keys are available (`?` denotes unavailable or encrypted keys). + public keys are available (`?` denotes unavailable or encrypted keys). Coldkeys and hotkeys are listed in natural + sort order (e.g. coldkey2 before coldkey10). # EXAMPLE [green]$[/green] btcli wallet list --path ~/.bittensor + [green]$[/green] btcli w list --coldkeys-only [bold]NOTE[/bold]: This command is read-only and does not modify the filesystem or the blockchain state. It is intended for use with the Bittensor CLI to provide a quick overview of the user's wallets. """ @@ -2613,6 +2620,7 @@ def wallet_list( wallet.path, json_output, wallet_name=wallet_name, + coldkeys_only=coldkeys_only, ) ) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index b2bdcc8fc..d56bbcffc 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -2,6 +2,7 @@ import hashlib import json import os +import re from collections import defaultdict from enum import Enum from typing import Generator, Optional, Union @@ -891,8 +892,26 @@ async def wallet_history(wallet: Wallet): console.print(table) +def _natural_sort_key(name: str) -> list[Union[str, int]]: + """Sort names with numeric chunks ordered naturally (item2 before item10).""" + return [ + int(part) if part.isdigit() else part.casefold() + for part in re.split(r"(\d+)", name) + ] + + +def _hotkey_sort_key(hkey: Optional[Wallet]) -> tuple[bool, list[Union[str, int]]]: + if hkey is None: + return True, [] + name = hkey.hotkey_str or hkey.name + return False, _natural_sort_key(name) + + async def wallet_list( - wallet_path: str, json_output: bool, wallet_name: Optional[str] = None + wallet_path: str, + json_output: bool, + wallet_name: Optional[str] = None, + coldkeys_only: bool = False, ): """Lists wallets.""" wallets = utils.get_coldkey_wallets_for_path(wallet_path) @@ -905,6 +924,8 @@ async def wallet_list( if not wallets: print_error(f"Wallet '{wallet_name}' not found in dir: {wallet_path}") + wallets = sorted(wallets, key=lambda wallet: _natural_sort_key(wallet.name)) + root = Tree("Wallets") main_data_dict = {"wallets": []} for wallet in wallets: @@ -933,8 +954,13 @@ async def wallet_list( "hotkeys": wallet_hotkeys, } main_data_dict["wallets"].append(wallet_dict) - hotkeys = utils.get_hotkey_wallets_for_wallet( - wallet, show_nulls=True, show_encrypted=True + if coldkeys_only: + continue + hotkeys = sorted( + utils.get_hotkey_wallets_for_wallet( + wallet, show_nulls=True, show_encrypted=True + ), + key=_hotkey_sort_key, ) for hkey in hotkeys: data = f"[bold red]Hotkey[/bold red][green] {hkey}[/green] (?)" diff --git a/tests/unit_tests/test_wallet_list.py b/tests/unit_tests/test_wallet_list.py new file mode 100644 index 000000000..ca246d126 --- /dev/null +++ b/tests/unit_tests/test_wallet_list.py @@ -0,0 +1,145 @@ +import json +import pytest +from unittest.mock import MagicMock, patch + +from .conftest import COLDKEY_SS58, HOTKEY_SS58 + +MODULE = "bittensor_cli.src.commands.wallets" + + +def _make_list_wallet(name: str = "coldkey1") -> MagicMock: + wallet = MagicMock() + wallet.name = name + wallet.coldkeypub_file.exists_on_device.return_value = True + wallet.coldkeypub_file.path = f"/tmp/{name}/coldkeypub.txt" + wallet.coldkeypub_file.is_encrypted.return_value = False + wallet.coldkeypub.ss58_address = COLDKEY_SS58 + wallet.coldkeypub.crypto_type = 1 + return wallet + + +def _make_hotkey_wallet(name: str = "default") -> MagicMock: + hotkey = MagicMock() + hotkey.name = name + hotkey.hotkey_str = name + hotkey.get_hotkey.return_value.ss58_address = HOTKEY_SS58 + hotkey.get_hotkey.return_value.crypto_type = 1 + return hotkey + + +def test_natural_sort_key_orders_numeric_suffixes(): + from bittensor_cli.src.commands.wallets import _natural_sort_key + + names = ["coldkey10", "coldkey2", "coldkey1", "zebra", "alice"] + assert sorted(names, key=_natural_sort_key) == [ + "alice", + "coldkey1", + "coldkey2", + "coldkey10", + "zebra", + ] + + +@pytest.mark.asyncio +async def test_wallet_list_sorts_coldkeys_naturally_in_json_output(): + from bittensor_cli.src.commands.wallets import wallet_list + + wallets = [ + _make_list_wallet("coldkey10"), + _make_list_wallet("coldkey2"), + _make_list_wallet("coldkey1"), + ] + + with ( + patch(f"{MODULE}.utils.get_coldkey_wallets_for_path", return_value=wallets), + patch(f"{MODULE}.json_console") as mock_json, + ): + await wallet_list("/tmp/wallets", json_output=True, coldkeys_only=True) + + payload = json.loads(mock_json.print.call_args[0][0]) + assert [wallet["name"] for wallet in payload["wallets"]] == [ + "coldkey1", + "coldkey2", + "coldkey10", + ] + + +@pytest.mark.asyncio +async def test_wallet_list_sorts_hotkeys_naturally_in_json_output(): + from bittensor_cli.src.commands.wallets import wallet_list + + coldkey = _make_list_wallet() + hotkeys = [ + _make_hotkey_wallet("hotkey10"), + _make_hotkey_wallet("hotkey2"), + _make_hotkey_wallet("hotkey1"), + ] + + with ( + patch(f"{MODULE}.utils.get_coldkey_wallets_for_path", return_value=[coldkey]), + patch(f"{MODULE}.utils.get_hotkey_wallets_for_wallet", return_value=hotkeys), + patch(f"{MODULE}.json_console") as mock_json, + ): + await wallet_list("/tmp/wallets", json_output=True, coldkeys_only=False) + + payload = json.loads(mock_json.print.call_args[0][0]) + assert [hk["name"] for hk in payload["wallets"][0]["hotkeys"]] == [ + "hotkey1", + "hotkey2", + "hotkey10", + ] + + +@pytest.mark.asyncio +async def test_wallet_list_coldkeys_only_skips_hotkey_lookup(): + from bittensor_cli.src.commands.wallets import wallet_list + + coldkey = _make_list_wallet() + + with ( + patch(f"{MODULE}.utils.get_coldkey_wallets_for_path", return_value=[coldkey]), + patch(f"{MODULE}.utils.get_hotkey_wallets_for_wallet") as mock_hotkeys, + patch(f"{MODULE}.console"), + ): + await wallet_list("/tmp/wallets", json_output=False, coldkeys_only=True) + + mock_hotkeys.assert_not_called() + + +@pytest.mark.asyncio +async def test_wallet_list_default_includes_hotkeys(): + from bittensor_cli.src.commands.wallets import wallet_list + + coldkey = _make_list_wallet() + hotkey = _make_hotkey_wallet() + + with ( + patch(f"{MODULE}.utils.get_coldkey_wallets_for_path", return_value=[coldkey]), + patch( + f"{MODULE}.utils.get_hotkey_wallets_for_wallet", return_value=[hotkey] + ) as mock_hotkeys, + patch(f"{MODULE}.console"), + ): + await wallet_list("/tmp/wallets", json_output=False, coldkeys_only=False) + + mock_hotkeys.assert_called_once_with(coldkey, show_nulls=True, show_encrypted=True) + + +@pytest.mark.asyncio +async def test_wallet_list_coldkeys_only_json_has_empty_hotkeys(): + from bittensor_cli.src.commands.wallets import wallet_list + + coldkey = _make_list_wallet() + + with ( + patch(f"{MODULE}.utils.get_coldkey_wallets_for_path", return_value=[coldkey]), + patch(f"{MODULE}.utils.get_hotkey_wallets_for_wallet") as mock_hotkeys, + patch(f"{MODULE}.json_console") as mock_json, + ): + await wallet_list("/tmp/wallets", json_output=True, coldkeys_only=True) + + mock_hotkeys.assert_not_called() + payload = json.loads(mock_json.print.call_args[0][0]) + assert len(payload["wallets"]) == 1 + assert payload["wallets"][0]["name"] == "coldkey1" + assert payload["wallets"][0]["hotkeys"] == [] From a387a3bdd0ebd3062b5fab8bba270d4e9bccd022 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 19 Jun 2026 08:38:32 -0700 Subject: [PATCH 17/17] bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 95222a946..b2acabbc7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi" [project] name = "bittensor-cli" -version = "9.22.2" +version = "9.23.0rc1" description = "Bittensor CLI" readme = "README.md" authors = [