diff --git a/CHANGELOG.md b/CHANGELOG.md index cded2eb43..2b5e82c36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 9.8.1/2025-07-08 + +## What's Changed +* Fixed broken type annotation. by @thewhaleking in https://github.com/opentensor/btcli/pull/523 +* Update/slippage price calcs by @ibraheem-abe in https://github.com/opentensor/btcli/pull/526 +* Partially fix slippage display by @gztensor in https://github.com/opentensor/btcli/pull/524 +* stake add: netuid 0 by @thewhaleking in https://github.com/opentensor/btcli/pull/525 + +## New Contributors +* @gztensor made their first contribution in https://github.com/opentensor/btcli/pull/524 + +**Full Changelog**: https://github.com/opentensor/btcli/compare/v9.8.0...v9.8.1 + ## 9.8.0/2025-07-07 ## What's Changed diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 9128f83fe..bb1c5126a 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -3352,7 +3352,7 @@ def stake_add( ) else: netuid_ = get_optional_netuid(None, all_netuids) - netuids = [netuid_] if netuid_ else None + netuids = [netuid_] if netuid_ is not None else None if netuids: for netuid_ in netuids: # ensure no negative netuids make it into our list diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index c274246ff..e5b5eac12 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1423,23 +1423,40 @@ async def get_stake_for_coldkeys( return stake_info_map if stake_info_map else None async def all_subnets(self, block_hash: Optional[str] = None) -> list[DynamicInfo]: - result = await self.query_runtime_api( - "SubnetInfoRuntimeApi", - "get_all_dynamic_info", - block_hash=block_hash, + result, prices = await asyncio.gather( + self.query_runtime_api( + "SubnetInfoRuntimeApi", + "get_all_dynamic_info", + block_hash=block_hash, + ), + self.get_subnet_prices(block_hash=block_hash, page_size=129), ) - return DynamicInfo.list_from_any(result) + sns: list[DynamicInfo] = DynamicInfo.list_from_any(result) + for sn in sns: + if sn.netuid == 0: + sn.price = Balance.from_tao(1.0) + else: + try: + sn.price = prices[sn.netuid] + except KeyError: + sn.price = sn.tao_in / sn.alpha_in + return sns async def subnet( self, netuid: int, block_hash: Optional[str] = None ) -> "DynamicInfo": - result = await self.query_runtime_api( - "SubnetInfoRuntimeApi", - "get_dynamic_info", - params=[netuid], - block_hash=block_hash, + result, price = await asyncio.gather( + self.query_runtime_api( + "SubnetInfoRuntimeApi", + "get_dynamic_info", + params=[netuid], + block_hash=block_hash, + ), + self.get_subnet_price(netuid=netuid, block_hash=block_hash), ) - return DynamicInfo.from_any(result) + subnet_ = DynamicInfo.from_any(result) + subnet_.price = price + return subnet_ async def get_owned_hotkeys( self, @@ -1581,3 +1598,53 @@ async def get_coldkey_swap_schedule_duration( ) return result + + async def get_subnet_price( + self, + netuid: int = None, + block_hash: Optional[str] = None, + ) -> Balance: + """ + Gets the current Alpha price in TAO for a specific subnet. + + :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. + """ + 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_prices( + self, block_hash: Optional[str] = None, page_size: int = 100 + ) -> dict[int, Balance]: + """ + Gets the current Alpha prices in TAO for all subnets. + + :param block_hash: The hash of the block to retrieve prices from. + :param page_size: The page size for batch queries (default: 100). + + :return: A dictionary mapping netuid to the current Alpha price in TAO units. + """ + query = await self.substrate.query_map( + module="Swap", + storage_function="AlphaSqrtPrice", + page_size=page_size, + block_hash=block_hash, + ) + + map_ = {} + async for netuid_, current_sqrt_price in query: + current_sqrt_price_ = fixed_to_float(current_sqrt_price.value) + current_price = current_sqrt_price_**2 + map_[netuid_] = Balance.from_rao(int(current_price * 1e9)) + + return map_ diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index a8d1ecc59..45b17dfbf 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -245,12 +245,12 @@ async def stake_extrinsic( # Get subnet data and stake information for coldkey chain_head = await subtensor.substrate.get_chain_head() _all_subnets, _stake_info, current_wallet_balance = await asyncio.gather( - subtensor.all_subnets(), + subtensor.all_subnets(block_hash=chain_head), subtensor.get_stake_for_coldkey( coldkey_ss58=wallet.coldkeypub.ss58_address, block_hash=chain_head, ), - subtensor.get_balance(wallet.coldkeypub.ss58_address), + subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash=chain_head), ) all_subnets = {di.netuid: di for di in _all_subnets} @@ -307,6 +307,7 @@ async def stake_extrinsic( return False remaining_wallet_balance -= amount_to_stake + # TODO this should be asyncio gathered before the for loop stake_fee = await subtensor.get_stake_fee( origin_hotkey_ss58=None, origin_netuid=None, @@ -318,14 +319,20 @@ async def stake_extrinsic( ) # Calculate slippage - try: - received_amount, slippage_pct, slippage_pct_float, rate = ( - _calculate_slippage(subnet_info, amount_to_stake, stake_fee) - ) - except ValueError: - return False - - max_slippage = max(slippage_pct_float, max_slippage) + # TODO: Update for V3, slippage calculation is significantly different in v3 + # try: + # received_amount, slippage_pct, slippage_pct_float, rate = ( + # _calculate_slippage(subnet_info, amount_to_stake, stake_fee) + # ) + # except ValueError: + # return False + # + # max_slippage = max(slippage_pct_float, max_slippage) + + # Temporary workaround - calculations without slippage + current_price_float = float(subnet_info.price.tao) + rate = 1.0 / current_price_float + received_amount = rate * amount_to_stake # Add rows for the table base_row = [ @@ -336,19 +343,19 @@ async def stake_extrinsic( + f" {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ", # rate str(received_amount.set_unit(netuid)), # received str(stake_fee), # fee - str(slippage_pct), # slippage + # str(slippage_pct), # slippage ] # If we are staking safe, add price tolerance if safe_staking: if subnet_info.is_dynamic: - rate = amount_to_stake.rao / received_amount.rao - _rate_with_tolerance = rate * ( - 1 + rate_tolerance + price_with_tolerance = current_price_float * (1 + rate_tolerance) + _rate_with_tolerance = ( + 1.0 / price_with_tolerance ) # Rate only for display rate_with_tolerance = f"{_rate_with_tolerance:.4f}" price_with_tolerance = Balance.from_tao( - _rate_with_tolerance + price_with_tolerance ).rao # Actual price to pass to extrinsic else: rate_with_tolerance = "1" @@ -581,9 +588,10 @@ def _define_stake_table( justify="center", style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"], ) - table.add_column( - "Slippage", justify="center", style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"] - ) + # TODO: Uncomment when slippage is reimplemented for v3 + # table.add_column( + # "Slippage", justify="center", style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"] + # ) if safe_staking: table.add_column( @@ -628,8 +636,8 @@ def _print_table_and_slippage(table: Table, max_slippage: float, safe_staking: b - [bold white]Hotkey[/bold white]: The ss58 address of the hotkey you are staking to. - [bold white]Amount[/bold white]: The TAO you are staking into this subnet onto this hotkey. - [bold white]Rate[/bold white]: The rate of exchange between your TAO and the subnet's stake. - - [bold white]Received[/bold white]: The amount of stake you will receive on this subnet after slippage. - - [bold white]Slippage[/bold white]: The slippage percentage of the stake operation. (0% if the subnet is not dynamic i.e. root).""" + - [bold white]Received[/bold white]: The amount of stake you will receive on this subnet after slippage.""" + # - [bold white]Slippage[/bold white]: The slippage percentage of the stake operation. (0% if the subnet is not dynamic i.e. root).""" safe_staking_description = """ - [bold white]Rate Tolerance[/bold white]: Maximum acceptable alpha rate. If the rate exceeds this tolerance, the transaction will be limited or rejected. @@ -654,6 +662,9 @@ def _calculate_slippage( - slippage_str: Formatted slippage percentage string - slippage_float: Raw slippage percentage value - rate: Exchange rate string + + TODO: Update to v3. This method only works for protocol-liquidity-only + mode (user liquidity disabled) """ amount_after_fee = amount - stake_fee @@ -670,6 +681,7 @@ def _calculate_slippage( slippage_str = f"{slippage_pct_float:.4f} %" rate = f"{(1 / subnet_info.price.tao or 1):.4f}" else: + # TODO: Fix this. Slippage is always zero for static networks. slippage_pct_float = ( 100 * float(stake_fee.tao) / float(amount.tao) if amount.tao != 0 else 0 ) diff --git a/bittensor_cli/src/commands/stake/list.py b/bittensor_cli/src/commands/stake/list.py index 2e6d39d76..4a8e17145 100644 --- a/bittensor_cli/src/commands/stake/list.py +++ b/bittensor_cli/src/commands/stake/list.py @@ -111,15 +111,15 @@ def define_table( style=COLOR_PALETTE["POOLS"]["RATE"], justify="center", ) - defined_table.add_column( - f"[white]Swap ({Balance.get_unit(1)} -> {Balance.unit})", - footer_style="overline white", - style=COLOR_PALETTE["STAKE"]["STAKE_SWAP"], - justify="right", - footer=f"τ {millify_tao(total_swapped_tao_value_.tao)}" - if not verbose - else f"{total_swapped_tao_value_}", - ) + # defined_table.add_column( + # f"[white]Swap ({Balance.get_unit(1)} -> {Balance.unit})", + # footer_style="overline white", + # style=COLOR_PALETTE["STAKE"]["STAKE_SWAP"], + # justify="right", + # footer=f"τ {millify_tao(total_swapped_tao_value_.tao)}" + # if not verbose + # else f"{total_swapped_tao_value_}", + # ) defined_table.add_column( "[white]Registered", style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], @@ -168,25 +168,17 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): tao_value_ = pool.alpha_to_tao(alpha_value) total_tao_value_ += tao_value_ - # Swapped TAO value and slippage cell - swapped_tao_value_, _, slippage_percentage_ = ( - pool.alpha_to_tao_with_slippage(substake_.stake) - ) - total_swapped_tao_value_ += swapped_tao_value_ - - # Slippage percentage cell - if pool.is_dynamic: - slippage_percentage = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{slippage_percentage_:.3f}%[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]" - else: - slippage_percentage = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]0.000%[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]" + # TAO value cell + tao_value_ = pool.alpha_to_tao(substake_.stake) + total_swapped_tao_value_ += tao_value_ if netuid == 0: - swap_value = f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]N/A[/{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}] ({slippage_percentage})" + swap_value = f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]N/A[/{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]" else: swap_value = ( - f"τ {millify_tao(swapped_tao_value_.tao)} ({slippage_percentage})" + f"τ {millify_tao(tao_value_.tao)}" if not verbose - else f"{swapped_tao_value_} ({slippage_percentage})" + else f"{tao_value_}" ) # Per block emission cell @@ -214,7 +206,7 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): else f"{symbol} {stake_value}", # Stake (a) f"{pool.price.tao:.4f} τ/{symbol}", # Rate (t/a) # f"τ {millify_tao(tao_ownership.tao)}" if not verbose else f"{tao_ownership}", # TAO equiv - swap_value, # Swap(α) -> τ + # swap_value, # Swap(α) -> τ "YES" if substake_.is_registered else f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]NO", # Registered @@ -232,7 +224,7 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): "value": tao_value_.tao, "stake_value": substake_.stake.tao, "rate": pool.price.tao, - "swap_value": swap_value, + # "swap_value": swap_value, "registered": True if substake_.is_registered else False, "emission": { "alpha": per_block_emission, @@ -317,9 +309,7 @@ def format_cell( alpha_value = Balance.from_rao(int(substake_.stake.rao)).set_unit(netuid) tao_value_ = pool.alpha_to_tao(alpha_value) total_tao_value_ += tao_value_ - swapped_tao_value_, slippage, slippage_pct = ( - pool.alpha_to_tao_with_slippage(substake_.stake) - ) + swapped_tao_value_ = pool.alpha_to_tao(substake_.stake) total_swapped_tao_value_ += swapped_tao_value_ # Store current values for future delta tracking @@ -364,19 +354,16 @@ def format_cell( ) if netuid != 0: - swap_cell = ( - format_cell( - swapped_tao_value_.tao, - prev.get("swapped_value"), - unit="τ", - unit_first_=True, - precision=4, - millify=True if not verbose else False, - ) - + f" ({slippage_pct:.2f}%)" + swap_cell = format_cell( + swapped_tao_value_.tao, + prev.get("swapped_value"), + unit="τ", + unit_first_=True, + precision=4, + millify=True if not verbose else False, ) else: - swap_cell = f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]N/A[/{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}] ({slippage_pct}%)" + swap_cell = f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]N/A[/{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]" emission_value = substake_.emission.tao / (pool.tempo or 1) emission_cell = format_cell( @@ -408,7 +395,7 @@ def format_cell( exchange_cell, # Exchange value stake_cell, # Stake amount rate_cell, # Rate - swap_cell, # Swap value with slippage + # swap_cell, # Swap value "YES" if substake_.is_registered else f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]NO", # Registration status @@ -591,12 +578,12 @@ def format_cell( f"Wallet:\n" f" Coldkey SS58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{coldkey_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" f" Free Balance: [{COLOR_PALETTE['GENERAL']['BALANCE']}]{balance}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\n" - f" Total TAO Value ({Balance.unit}): [{COLOR_PALETTE['GENERAL']['BALANCE']}]{total_tao_value}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\n" - f" Total TAO Swapped Value ({Balance.unit}): [{COLOR_PALETTE['GENERAL']['BALANCE']}]{total_swapped_tao_value}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]" + f" Total TAO Value ({Balance.unit}): [{COLOR_PALETTE['GENERAL']['BALANCE']}]{total_tao_value}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]" + # f"\n Total TAO Swapped Value ({Balance.unit}): [{COLOR_PALETTE['GENERAL']['BALANCE']}]{total_swapped_tao_value}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]" ) dict_output["free_balance"] = balance.tao dict_output["total_tao_value"] = all_hks_tao_value.tao - dict_output["total_swapped_tao_value"] = all_hks_swapped_tao_value.tao + # dict_output["total_swapped_tao_value"] = all_hks_swapped_tao_value.tao if json_output: json_console.print(json.dumps(dict_output)) if not sub_stakes: @@ -658,12 +645,12 @@ def format_cell( ), ( "[bold tan]Exchange Value (α x τ/α)[/bold tan]", - "This is the potential τ you will receive, without considering slippage, if you unstake from this hotkey now on this subnet. See Swap(α → τ) column description. Note: The TAO Equiv(τ_in x α/α_out) indicates validator stake weight while this Exchange Value shows τ you will receive if you unstake now. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#exchange-value-%CE%B1-x-%CF%84%CE%B1[/blue].", - ), - ( - "[bold tan]Swap (α → τ)[/bold tan]", - "This is the actual τ you will receive, after factoring in the slippage charge, if you unstake from this hotkey now on this subnet. The slippage is calculated as 1 - (Swap(α → τ)/Exchange Value(α x τ/α)), and is displayed in brackets. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#swap-%CE%B1--%CF%84[/blue].", + "This is the potential τ you will receive if you unstake from this hotkey now on this subnet. Note: The TAO Equiv(τ_in x α/α_out) indicates validator stake weight while this Exchange Value shows τ you will receive if you unstake now. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#exchange-value-%CE%B1-x-%CF%84%CE%B1[/blue].", ), + # ( + # "[bold tan]Swap (α → τ)[/bold tan]", + # "This is the τ you will receive if you unstake from this hotkey now on this subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#swap-%CE%B1--%CF%84[/blue].", + # ), ( "[bold tan]Registered[/bold tan]", "Indicates if the hotkey is registered in this subnet or not. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index 42f61934b..482dc70b5 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -33,8 +33,8 @@ async def display_stake_movement_cross_subnets( destination_hotkey: str, amount_to_move: Balance, stake_fee: Balance, -) -> tuple[Balance, float, str, str]: - """Calculate and display slippage information""" +) -> tuple[Balance, str]: + """Calculate and display stake movement information""" if origin_netuid == destination_netuid: subnet = await subtensor.subnet(origin_netuid) @@ -46,45 +46,32 @@ async def display_stake_movement_cross_subnets( raise ValueError received_amount = subnet.tao_to_alpha(received_amount_tao) - slippage_pct_float = ( - 100 * float(stake_fee) / float(stake_fee + received_amount_tao) - if received_amount_tao != 0 - else 0 - ) - slippage_pct = f"{slippage_pct_float:.4f}%" - price = Balance.from_tao(1).set_unit(origin_netuid) + price = subnet.price.tao price_str = ( - str(float(price.tao)) - + f"{Balance.get_unit(origin_netuid)}/{Balance.get_unit(origin_netuid)}" + str(float(price)) + + f"({Balance.get_unit(0)}/{Balance.get_unit(origin_netuid)})" ) else: dynamic_origin, dynamic_destination = await asyncio.gather( subtensor.subnet(origin_netuid), subtensor.subnet(destination_netuid), ) - price = ( - float(dynamic_origin.price) * 1 / (float(dynamic_destination.price) or 1) - ) - received_amount_tao, _, _ = dynamic_origin.alpha_to_tao_with_slippage( - amount_to_move - ) + price_origin = dynamic_origin.price.tao + price_destination = dynamic_destination.price.tao + rate = price_origin / (price_destination or 1) + + received_amount_tao = dynamic_origin.alpha_to_tao(amount_to_move) received_amount_tao -= stake_fee - received_amount, _, _ = dynamic_destination.tao_to_alpha_with_slippage( - received_amount_tao - ) + received_amount = dynamic_destination.tao_to_alpha(received_amount_tao) received_amount.set_unit(destination_netuid) if received_amount < Balance.from_tao(0): print_error("Not enough Alpha to pay the transaction fee.") raise ValueError - ideal_amount = amount_to_move * price - total_slippage = ideal_amount - received_amount - slippage_pct_float = 100 * (total_slippage.tao / ideal_amount.tao) - slippage_pct = f"{slippage_pct_float:.4f} %" price_str = ( - f"{price:.5f}" - + f"{Balance.get_unit(destination_netuid)}/{Balance.get_unit(origin_netuid)}" + f"{rate:.5f}" + + f"({Balance.get_unit(destination_netuid)}/{Balance.get_unit(origin_netuid)})" ) # Create and display table @@ -141,11 +128,6 @@ async def display_stake_movement_cross_subnets( justify="center", style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"], ) - table.add_column( - "slippage", - justify="center", - style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"], - ) table.add_row( f"{Balance.get_unit(origin_netuid)}({origin_netuid})", @@ -156,19 +138,11 @@ async def display_stake_movement_cross_subnets( price_str, str(received_amount), str(stake_fee), - str(slippage_pct), ) console.print(table) - # Display slippage warning if necessary - if slippage_pct_float > 5: - message = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n" - message += f"[bold]WARNING:\tSlippage is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{slippage_pct}[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}], this may result in a loss of funds.[/bold] \n" - message += "-------------------------------------------------------------------------------------------------------------------\n" - console.print(message) - - return received_amount, slippage_pct_float, slippage_pct, price_str + return received_amount, price_str def prompt_stake_amount( @@ -414,7 +388,7 @@ async def stake_swap_selection( origin_stake = hotkey_stakes[origin_netuid]["stake"] # Ask for amount to swap - amount, all_balance = prompt_stake_amount(origin_stake, origin_netuid, "swap") + amount, _ = prompt_stake_amount(origin_stake, origin_netuid, "swap") all_netuids = sorted(await subtensor.get_all_subnet_netuids()) destination_choices = [ @@ -530,7 +504,7 @@ async def move_stake( amount=amount_to_move_as_balance.rao, ) - # Slippage warning + # Display stake movement details if prompt: try: await display_stake_movement_cross_subnets( @@ -714,7 +688,7 @@ async def transfer_stake( amount=amount_to_transfer.rao, ) - # Slippage warning + # Display stake movement details if prompt: try: await display_stake_movement_cross_subnets( @@ -883,7 +857,7 @@ async def swap_stake( amount=amount_to_swap.rao, ) - # Slippage warning + # Display stake movement details if prompt: try: await display_stake_movement_cross_subnets( diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index b2ef8b608..617b8581b 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -209,16 +209,12 @@ async def unstake( ) try: - received_amount, slippage_pct, slippage_pct_float = _calculate_slippage( - subnet_info=subnet_info, - amount=amount_to_unstake_as_balance, - stake_fee=stake_fee, - ) + current_price = subnet_info.price.tao + rate = current_price + received_amount = amount_to_unstake_as_balance * rate except ValueError: continue - total_received_amount += received_amount - max_float_slippage = max(max_float_slippage, slippage_pct_float) base_unstake_op = { "netuid": netuid, @@ -229,8 +225,6 @@ async def unstake( "amount_to_unstake": amount_to_unstake_as_balance, "current_stake_balance": current_stake_balance, "received_amount": received_amount, - "slippage_pct": slippage_pct, - "slippage_pct_float": slippage_pct_float, "dynamic_info": subnet_info, } @@ -238,20 +232,18 @@ async def unstake( str(netuid), # Netuid staking_address_name, # Hotkey Name str(amount_to_unstake_as_balance), # Amount to Unstake - str(subnet_info.price.tao) + f"{subnet_info.price.tao:.6f}" + f"({Balance.get_unit(0)}/{Balance.get_unit(netuid)})", # Rate str(stake_fee), # Fee str(received_amount), # Received Amount - slippage_pct, # Slippage Percent + # slippage_pct, # Slippage Percent ] # Additional fields for safe unstaking if safe_staking: if subnet_info.is_dynamic: - rate = received_amount.rao / amount_to_unstake_as_balance.rao - rate_with_tolerance = rate * ( - 1 - rate_tolerance - ) # Rate only for display + price_with_tolerance = current_price * (1 - rate_tolerance) + rate_with_tolerance = price_with_tolerance price_with_tolerance = Balance.from_tao( rate_with_tolerance ).rao # Actual price to pass to extrinsic @@ -263,7 +255,7 @@ async def unstake( base_table_row.extend( [ # Rate with tolerance - f"{rate_with_tolerance:.4f} {Balance.get_unit(0)}/{Balance.get_unit(netuid)}", + f"{rate_with_tolerance:.6f} {Balance.get_unit(0)}/{Balance.get_unit(netuid)}", # Partial unstake f"[{'dark_sea_green3' if allow_partial_stake else 'red'}]" f"{allow_partial_stake}[/{'dark_sea_green3' if allow_partial_stake else 'red'}]", @@ -448,14 +440,13 @@ async def unstake_all( justify="center", style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], ) - table.add_column( - "Slippage", - justify="center", - style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"], - ) + # table.add_column( + # "Slippage", + # justify="center", + # style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"], + # ) - # Calculate slippage and total received - max_slippage = 0.0 + # Calculate total received total_received_value = Balance(0) for stake in stake_info: if stake.stake.rao == 0: @@ -473,41 +464,33 @@ async def unstake_all( destination_coldkey_ss58=wallet.coldkeypub.ss58_address, amount=stake_amount.rao, ) + try: - received_amount, slippage_pct, slippage_pct_float = _calculate_slippage( - subnet_info=subnet_info, amount=stake_amount, stake_fee=stake_fee - ) - except ValueError: + current_price = subnet_info.price.tao + rate = current_price + received_amount = stake_amount * rate - stake_fee + + if received_amount < Balance.from_tao(0): + print_error("Not enough Alpha to pay the transaction fee.") + continue + except (AttributeError, ValueError): continue - max_slippage = max(max_slippage, slippage_pct_float) total_received_value += received_amount table.add_row( str(stake.netuid), hotkey_display, str(stake_amount), - str(float(subnet_info.price)) + f"{float(subnet_info.price):.6f}" + f"({Balance.get_unit(0)}/{Balance.get_unit(stake.netuid)})", str(stake_fee), str(received_amount), - slippage_pct, ) console.print(table) - if max_slippage > 5: - message = ( - f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]--------------------------------------------------------------" - f"-----------------------------------------------------\n" - f"[bold]WARNING:[/bold] The slippage on one of your operations is high: " - f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{max_slippage:.4f}%" - f"[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}], this may result in a loss of funds.\n" - "----------------------------------------------------------------------------------------------------------" - "---------\n" - ) - console.print(message) console.print( - f"Expected return after slippage: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{total_received_value}" + f"Total expected return: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{total_received_value}" ) if prompt and not Confirm.ask( @@ -848,51 +831,6 @@ async def _unstake_all_extrinsic( # Helpers -def _calculate_slippage( - subnet_info, amount: Balance, stake_fee: Balance -) -> tuple[Balance, str, float]: - """Calculate slippage and received amount for unstaking operation. - - Args: - subnet_info: Subnet information containing price data - amount: Amount being unstaked - stake_fee: Stake fee to include in slippage calculation - - Returns: - tuple containing: - - received_amount: Balance after slippage deduction - - slippage_pct: Formatted string of slippage percentage - - slippage_pct_float: Float value of slippage percentage - """ - received_amount, _, _ = subnet_info.alpha_to_tao_with_slippage(amount) - received_amount -= stake_fee - - if received_amount < Balance.from_tao(0): - print_error("Not enough Alpha to pay the transaction fee.") - raise ValueError - - if subnet_info.is_dynamic: - # Ideal amount w/o slippage - ideal_amount = subnet_info.alpha_to_tao(amount) - - # Total slippage including fees - total_slippage = ideal_amount - received_amount - slippage_pct_float = ( - 100 * (float(total_slippage.tao) / float(ideal_amount.tao)) - if ideal_amount.tao != 0 - else 0 - ) - slippage_pct = f"{slippage_pct_float:.4f} %" - else: - # Root will only have fee-based slippage - slippage_pct_float = ( - 100 * float(stake_fee.tao) / float(amount.tao) if amount.tao != 0 else 0 - ) - slippage_pct = f"{slippage_pct_float:.4f} %" - - return received_amount, slippage_pct, slippage_pct_float - - async def _unstake_selection( dynamic_info, identities, @@ -993,14 +931,14 @@ async def _unstake_selection( table.add_column("Symbol", style=COLOR_PALETTE["GENERAL"]["SYMBOL"]) table.add_column("Stake Amount", style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"]) table.add_column( - f"[bold white]RATE ({Balance.get_unit(0)}_in/{Balance.get_unit(1)}_in)", + f"[bold white]Rate ({Balance.get_unit(0)}/{Balance.get_unit(1)})", style=COLOR_PALETTE["POOLS"]["RATE"], justify="left", ) for netuid_, stake_amount in netuid_stakes.items(): symbol = dynamic_info[netuid_].symbol - rate = f"{dynamic_info[netuid_].price.tao:.4f} τ/{symbol}" + rate = f"{dynamic_info[netuid_].price.tao:.6f} τ/{symbol}" table.add_row(str(netuid_), symbol, str(stake_amount), rate) console.print("\n", table, "\n") @@ -1261,9 +1199,9 @@ def _create_unstake_table( style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], footer=str(total_received_amount), ) - table.add_column( - "Slippage", justify="center", style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"] - ) + # table.add_column( + # "Slippage", justify="center", style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"] + # ) if safe_staking: table.add_column( f"Rate with tolerance: [blue]({rate_tolerance * 100}%)[/blue]", diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 8d7950392..88032df89 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -79,7 +79,7 @@ def allowed_value( return True, value -def string_to_bool(val) -> bool | type[ValueError]: +def string_to_bool(val) -> bool: try: return {"true": True, "1": True, "0": False, "false": False}[val.lower()] except KeyError: diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 1c2de6a3e..99842ef98 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -542,14 +542,12 @@ async def wallet_balance( total_free_balance = sum(free_balances.values()) total_staked_balance = sum(stake[0] for stake in staked_balances.values()) - total_staked_with_slippage = sum(stake[1] for stake in staked_balances.values()) balances = { name: ( coldkey, free_balances[coldkey], staked_balances[coldkey][0], - staked_balances[coldkey][1], ) for (name, coldkey) in zip(wallet_names, coldkeys) } @@ -577,24 +575,12 @@ async def wallet_balance( style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], no_wrap=True, ), - Column( - "[white]Staked (w/slippage)", - justify="right", - style=COLOR_PALETTE["STAKE"]["STAKE_SWAP"], - no_wrap=True, - ), Column( "[white]Total Balance", justify="right", style=COLOR_PALETTE["GENERAL"]["BALANCE"], no_wrap=True, ), - Column( - "[white]Total (w/slippage)", - justify="right", - style=COLOR_PALETTE["GENERAL"]["BALANCE"], - no_wrap=True, - ), title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Wallet Coldkey Balance[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Network: {subtensor.network}\n", show_footer=True, show_edge=False, @@ -605,15 +591,13 @@ async def wallet_balance( leading=True, ) - for name, (coldkey, free, staked, staked_slippage) in balances.items(): + for name, (coldkey, free, staked) in balances.items(): table.add_row( name, coldkey, str(free), str(staked), - str(staked_slippage), str(free + staked), - str(free + staked_slippage), ) table.add_row() table.add_row( @@ -621,9 +605,7 @@ async def wallet_balance( "", str(total_free_balance), str(total_staked_balance), - str(total_staked_with_slippage), str(total_free_balance + total_staked_balance), - str(total_free_balance + total_staked_with_slippage), ) console.print(Padding(table, (0, 0, 0, 4))) await subtensor.substrate.close() @@ -633,9 +615,7 @@ async def wallet_balance( "coldkey": value[0], "free": value[1].tao, "staked": value[2].tao, - "staked_with_slippage": value[3].tao, "total": (value[1] + value[2]).tao, - "total_with_slippage": (value[1] + value[3]).tao, } for (key, value) in balances.items() } @@ -644,11 +624,7 @@ async def wallet_balance( "totals": { "free": total_free_balance.tao, "staked": total_staked_balance.tao, - "staked_with_slippage": total_staked_with_slippage.tao, "total": (total_free_balance + total_staked_balance).tao, - "total_with_slippage": ( - total_free_balance + total_staked_with_slippage - ).tao, }, } json_console.print(json.dumps(output_dict)) diff --git a/pyproject.toml b/pyproject.toml index d733a105e..439448ce5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bittensor-cli" -version = "9.8.0" +version = "9.8.1" description = "Bittensor CLI" readme = "README.md" authors = [ diff --git a/tests/unit_tests/test_cli.py b/tests/unit_tests/test_cli.py index 34c9b2d3c..b7933e226 100644 --- a/tests/unit_tests/test_cli.py +++ b/tests/unit_tests/test_cli.py @@ -2,6 +2,7 @@ import typer from bittensor_cli.cli import parse_mnemonic +from unittest.mock import AsyncMock, patch, MagicMock def test_parse_mnemonic(): @@ -16,3 +17,37 @@ def test_parse_mnemonic(): parse_mnemonic("1-hello 1-how 2-are 3-you") # missing numbers parse_mnemonic("1-hello 3-are 4-you") + + +@pytest.mark.asyncio +async def test_subnet_sets_price_correctly(): + from bittensor_cli.src.bittensor.subtensor_interface import ( + SubtensorInterface, + DynamicInfo, + ) + + mock_result = {"some": "data"} + mock_price = 42.0 + mock_dynamic_info = MagicMock() + mock_dynamic_info.price = None + + with ( + patch.object( + SubtensorInterface, "query_runtime_api", new_callable=AsyncMock + ) as mock_query, + patch.object( + SubtensorInterface, "get_subnet_price", new_callable=AsyncMock + ) as mock_price_method, + patch.object(DynamicInfo, "from_any", return_value=mock_dynamic_info), + ): + mock_query.return_value = mock_result + mock_price_method.return_value = mock_price + + subtensor = SubtensorInterface("finney") + subnet_info = await subtensor.subnet(netuid=1) + + mock_query.assert_awaited_once_with( + "SubnetInfoRuntimeApi", "get_dynamic_info", params=[1], block_hash=None + ) + mock_price_method.assert_awaited_once_with(netuid=1, block_hash=None) + assert subnet_info.price == mock_price