From a490bb806230204e1da2c9ce58a5e7bda423b794 Mon Sep 17 00:00:00 2001 From: Ibraheem <165814940+ibraheem-abe@users.noreply.github.com> Date: Tue, 24 Feb 2026 08:15:10 -0800 Subject: [PATCH 1/3] Revert "Feat/balancer swap updates" --- bittensor_cli/cli.py | 14 +-- bittensor_cli/src/bittensor/chain_data.py | 98 ++++++++++++++++--- .../src/bittensor/subtensor_interface.py | 94 +++++++++--------- bittensor_cli/src/commands/stake/add.py | 76 +++++++++++--- bittensor_cli/src/commands/stake/remove.py | 30 ++---- tests/e2e_tests/test_staking_sudo.py | 4 +- 6 files changed, 204 insertions(+), 112 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 47c005c34..77b5bd3c5 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -32,7 +32,7 @@ from rich.prompt import FloatPrompt, Prompt, IntPrompt from rich.table import Column, Table from rich.tree import Tree -from typing import Annotated +from typing_extensions import Annotated from yaml import safe_dump, safe_load from bittensor_cli.src import ( @@ -867,7 +867,7 @@ 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, hidden=True) + 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) @@ -8672,9 +8672,6 @@ def liquidity_add( json_output: bool = Options.json_output, ): """Add liquidity to the swap (as a combination of TAO + Alpha).""" - console.print_error("User liquidity is currently disabled on Bittensor.") - raise typer.Exit() - self.verbosity_handler(quiet, verbose, json_output, prompt, decline) proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if not netuid: @@ -8752,9 +8749,6 @@ def liquidity_list( json_output: bool = Options.json_output, ): """Displays liquidity positions in given subnet.""" - console.print_error("User liquidity is currently disabled on Bittensor.") - raise typer.Exit() - self.verbosity_handler(quiet, verbose, json_output, prompt=False) if not netuid: netuid = IntPrompt.ask( @@ -8808,8 +8802,6 @@ def liquidity_remove( ): """Remove liquidity from the swap (as a combination of TAO + Alpha).""" - console.print_error("User liquidity is currently disabled on Bittensor.") - raise typer.Exit() self.verbosity_handler(quiet, verbose, json_output, prompt, decline) proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if all_liquidity_ids and position_id: @@ -8886,8 +8878,6 @@ def liquidity_modify( json_output: bool = Options.json_output, ): """Modifies the liquidity position for the given subnet.""" - console.print_error("User liquidity is currently disabled on Bittensor.") - raise typer.Exit() self.verbosity_handler(quiet, verbose, json_output, prompt, decline) proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if not netuid: diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index e33b141ea..718dbd694 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -800,6 +800,88 @@ def tao_to_alpha(self, tao: Balance) -> Balance: def alpha_to_tao(self, alpha: Balance) -> Balance: return Balance.from_tao(alpha.tao * self.price.tao) + def tao_to_alpha_with_slippage( + self, tao: Balance + ) -> tuple[Balance, Balance, float]: + """ + Returns an estimate of how much Alpha a staker would receive if they stake their tao using the current pool + state. + + Args: + tao: Amount of TAO to stake. + Returns: + Tuple of balances where the first part is the amount of Alpha received, and the + second part (slippage) is the difference between the estimated amount and ideal + amount as if there was no slippage + """ + if self.is_dynamic: + new_tao_in = self.tao_in + tao + if new_tao_in == 0: + return tao, Balance.from_rao(0) + new_alpha_in = self.k / new_tao_in + + # Amount of alpha given to the staker + alpha_returned = Balance.from_rao( + self.alpha_in.rao - new_alpha_in.rao + ).set_unit(self.netuid) + + # Ideal conversion as if there is no slippage, just price + alpha_ideal = self.tao_to_alpha(tao) + + if alpha_ideal.tao > alpha_returned.tao: + slippage = Balance.from_tao( + alpha_ideal.tao - alpha_returned.tao + ).set_unit(self.netuid) + else: + slippage = Balance.from_tao(0) + else: + alpha_returned = tao.set_unit(self.netuid) + slippage = Balance.from_tao(0) + + slippage_pct_float = ( + 100 * float(slippage) / float(slippage + alpha_returned) + if slippage + alpha_returned != 0 + else 0 + ) + return alpha_returned, slippage, slippage_pct_float + + def alpha_to_tao_with_slippage( + self, alpha: Balance + ) -> tuple[Balance, Balance, float]: + """ + Returns an estimate of how much TAO a staker would receive if they unstake their alpha using the current pool + state. + + Args: + alpha: Amount of Alpha to stake. + Returns: + Tuple of balances where the first part is the amount of TAO received, and the + second part (slippage) is the difference between the estimated amount and ideal + amount as if there was no slippage + """ + if self.is_dynamic: + new_alpha_in = self.alpha_in + alpha + new_tao_reserve = self.k / new_alpha_in + # Amount of TAO given to the unstaker + tao_returned = Balance.from_rao(self.tao_in - new_tao_reserve) + + # Ideal conversion as if there is no slippage, just price + tao_ideal = self.alpha_to_tao(alpha) + + if tao_ideal > tao_returned: + slippage = Balance.from_tao(tao_ideal.tao - tao_returned.tao) + else: + slippage = Balance.from_tao(0) + else: + tao_returned = alpha.set_unit(0) + slippage = Balance.from_tao(0) + slippage_pct_float = ( + 100 * float(slippage) / float(slippage + tao_returned) + if slippage + tao_returned != 0 + else 0 + ) + return tao_returned, slippage, slippage_pct_float + @dataclass class ColdkeySwapAnnouncementInfo(InfoBase): @@ -1136,8 +1218,6 @@ class SimSwapResult: alpha_amount: Balance tao_fee: Balance alpha_fee: Balance - tao_slippage: Balance - alpha_slippage: Balance @classmethod def from_dict(cls, d: dict, netuid: int) -> "SimSwapResult": @@ -1146,22 +1226,8 @@ def from_dict(cls, d: dict, netuid: int) -> "SimSwapResult": alpha_amount=Balance.from_rao(d["alpha_amount"]).set_unit(netuid), tao_fee=Balance.from_rao(d["tao_fee"]).set_unit(0), alpha_fee=Balance.from_rao(d["alpha_fee"]).set_unit(netuid), - tao_slippage=Balance.from_rao(d["tao_slippage"]).set_unit(0), - alpha_slippage=Balance.from_rao(d["alpha_slippage"]).set_unit(netuid), ) - @property - def tao_slippage_pct(self) -> float: - """Slippage percentage for alpha->tao swaps.""" - ideal = self.tao_amount.tao + self.tao_slippage.tao - return (100.0 * self.tao_slippage.tao / ideal) if ideal > 0 else 0.0 - - @property - def alpha_slippage_pct(self) -> float: - """Slippage percentage for tao->alpha swaps.""" - ideal = self.alpha_amount.tao + self.alpha_slippage.tao - return (100.0 * self.alpha_slippage.tao / ideal) if ideal > 0 else 0.0 - @dataclass class CrowdloanData(InfoBase): diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 7eb93aa7c..8b393c884 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -444,52 +444,40 @@ async def get_total_stake_for_coldkey( :return: {address: Balance objects} """ - sub_stakes, price_map = await asyncio.gather( + sub_stakes, dynamic_info = await asyncio.gather( self.get_stake_for_coldkeys(list(ss58_addresses), block_hash=block_hash), - self.get_subnet_prices(block_hash=block_hash), + # Token pricing info + self.all_subnets(block_hash=block_hash), ) - results: dict[str, tuple[Balance, Balance]] = {} - dynamic_stakes: list[tuple[str, "StakeInfo"]] = [] - + results = {} for ss58, stake_info_list in sub_stakes.items(): - total_tao_value, total_swapped_tao_value = Balance(0), Balance(0) + total_tao_value = Balance(0) + total_swapped_tao_value = Balance(0) for sub_stake in stake_info_list: if sub_stake.stake.rao == 0: continue - netuid = sub_stake.netuid - price = price_map[netuid] - ideal_tao = Balance.from_tao(sub_stake.stake.tao * price.tao).set_unit( - 0 + pool = dynamic_info[netuid] + + alpha_value = Balance.from_rao(int(sub_stake.stake.rao)).set_unit( + netuid ) - total_tao_value += ideal_tao + # Without slippage + tao_value = pool.alpha_to_tao(alpha_value) + total_tao_value += tao_value + + # With slippage if netuid == 0: - total_swapped_tao_value += ideal_tao + swapped_tao_value = tao_value else: - dynamic_stakes.append((ss58, sub_stake)) - - results[ss58] = (total_tao_value, total_swapped_tao_value) - - if dynamic_stakes: - sim_results = await asyncio.gather( - *[ - self.sim_swap( - origin_netuid=sub_stake.netuid, - destination_netuid=0, - amount=sub_stake.stake.rao, - block_hash=block_hash, + swapped_tao_value, _, _ = pool.alpha_to_tao_with_slippage( + sub_stake.stake ) - for _, sub_stake in dynamic_stakes - ] - ) - - for (ss58, sub_stake), sim_result in zip(dynamic_stakes, sim_results): - total_tao_value, total_swapped_tao_value = results[ss58] - total_swapped_tao_value += sim_result.tao_amount - results[ss58] = (total_tao_value, total_swapped_tao_value) + total_swapped_tao_value += swapped_tao_value + results[ss58] = (total_tao_value, total_swapped_tao_value) return results async def get_total_stake_for_hotkey( @@ -1666,7 +1654,7 @@ async def all_subnets(self, block_hash: Optional[str] = None) -> list[DynamicInf "get_all_dynamic_info", block_hash=block_hash, ), - self.get_subnet_prices(block_hash=block_hash), + self.get_subnet_prices(block_hash=block_hash, page_size=129), ) sns: list[DynamicInfo] = DynamicInfo.list_from_any(result) for sn in sns: @@ -1794,7 +1782,6 @@ async def sim_swap( ) secondary_fee = (result.tao_fee / sn_price.tao).set_unit(origin_netuid) result.alpha_fee = result.alpha_fee + secondary_fee - result.tao_slippage = intermediate_result.tao_slippage return result elif origin_netuid > 0: # dynamic to tao @@ -2536,36 +2523,43 @@ async def get_subnet_price( :return: The current Alpha price in TAO units for the specified subnet. """ - if netuid == 0: - return Balance.from_tao(1.0) - - current_price = await self.query_runtime_api( - "SwapRuntimeApi", - "current_alpha_price", - params={"netuid": netuid}, + # 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, ) - return Balance.from_rao(current_price) + + 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 + 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. """ - all_prices = await self.query_runtime_api( - "SwapRuntimeApi", - "current_alpha_price_all", + query = await self.substrate.query_map( + module="Swap", + storage_function="AlphaSqrtPrice", + page_size=page_size, block_hash=block_hash, ) - result = {} - for entry in all_prices: - result[entry["netuid"]] = Balance.from_rao(entry["price"]) - return result + + 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_ async def get_all_subnet_ema_tao_inflow( self, diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 7372d6a21..424c44858 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -383,7 +383,18 @@ async def stake_extrinsic( return remaining_wallet_balance -= amount_to_stake - # Calculate rate + # Calculate 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 @@ -433,11 +444,6 @@ async def stake_extrinsic( amount=amount_minus_fee.rao, ) received_amount = sim_swap.alpha_amount - - slippage_pct_float = sim_swap.alpha_slippage_pct - slippage_pct = f"{slippage_pct_float:.4f} %" - max_slippage = max(slippage_pct_float, max_slippage) - # Add rows for the table base_row = [ str(netuid), # netuid @@ -448,7 +454,7 @@ async def stake_extrinsic( str(received_amount.set_unit(netuid)), # received str(sim_swap.tao_fee), # fee str(extrinsic_fee), - str(slippage_pct), # slippage + # str(slippage_pct), # slippage ] + row_extension rows.append(tuple(base_row)) @@ -683,9 +689,10 @@ def _define_stake_table( justify="center", style=COLOR_PALETTE.STAKE.TAO, ) - 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( @@ -730,11 +737,56 @@ 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. - [bold white]Partial staking[/bold white]: If True, allows staking up to the rate tolerance limit. If False, the entire transaction will fail if rate tolerance is exceeded.\n""" console.print(base_description + (safe_staking_description if safe_staking else "")) + + +def _calculate_slippage( + subnet_info, amount: Balance, stake_fee: Balance +) -> tuple[Balance, str, float, str]: + """Calculate slippage when adding stake. + + Args: + subnet_info: Subnet dynamic info + amount: Amount being staked + stake_fee: Transaction fee for the stake operation + + Returns: + tuple containing: + - received_amount: Amount received after slippage and fees + - 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 + + if amount_after_fee < 0: + print_error("You don't have enough balance to cover the stake fee.") + raise ValueError() + + received_amount, _, _ = subnet_info.tao_to_alpha_with_slippage(amount_after_fee) + + if subnet_info.is_dynamic: + ideal_amount = subnet_info.tao_to_alpha(amount) + total_slippage = ideal_amount - received_amount + slippage_pct_float = 100 * (total_slippage.tao / ideal_amount.tao) + 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 + ) + slippage_str = f"{slippage_pct_float:.4f} %" + rate = "1" + + return received_amount, slippage_str, slippage_pct_float, rate diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 9cc782be6..a06dbf8bb 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -250,10 +250,6 @@ async def unstake( received_amount = sim_swap.tao_amount if not proxy: received_amount -= extrinsic_fee - - slippage_pct_float = sim_swap.tao_slippage_pct - slippage_pct = f"{slippage_pct_float:.4f} %" - max_float_slippage = max(slippage_pct_float, max_float_slippage) except ValueError: continue total_received_amount += received_amount @@ -279,7 +275,7 @@ async def unstake( str(sim_swap.alpha_fee), # Fee str(extrinsic_fee), # Extrinsic fee str(received_amount), # Received Amount - slippage_pct, # Slippage Percent + # slippage_pct, # Slippage Percent ] # Additional fields for safe unstaking @@ -448,7 +444,6 @@ async def unstake_all( return all_sn_dynamic_info = {info.netuid: info for info in all_sn_dynamic_info_} - max_float_slippage = 0.0 # Create table for unstaking all table_title = ( @@ -493,11 +488,11 @@ 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 total received total_received_value = Balance(0) @@ -529,10 +524,6 @@ async def unstake_all( if received_amount < Balance.from_tao(0): print_error("Not enough Alpha to pay the transaction fee.") continue - - slippage_pct_float = sim_swap.tao_slippage_pct - slippage_pct = f"{slippage_pct_float:.4f} %" - max_float_slippage = max(slippage_pct_float, max_float_slippage) except (AttributeError, ValueError): continue @@ -547,9 +538,8 @@ async def unstake_all( str(sim_swap.alpha_fee), str(extrinsic_fee), str(received_amount), - slippage_pct, ) - _print_table_and_slippage(table, max_float_slippage, False) + console.print(table) console.print( f"Total expected return: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{total_received_value}" @@ -1361,9 +1351,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/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index 0e0cc65a4..e76ff1627 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -297,8 +297,8 @@ 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"] == 1 - assert stats["market_cap"] == 1_000 + assert stats["current_price"] == 0.0 + assert stats["market_cap"] == 0.0 # Start emissions on SNs for netuid_ in multiple_netuids: From 3b9cb29195055e36e50560b4ee81ed8f929a2043 Mon Sep 17 00:00:00 2001 From: Ibraheem <165814940+ibraheem-abe@users.noreply.github.com> Date: Tue, 24 Feb 2026 11:45:25 -0800 Subject: [PATCH 2/3] use typing --- bittensor_cli/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 77b5bd3c5..06651de93 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -32,7 +32,7 @@ from rich.prompt import FloatPrompt, Prompt, IntPrompt from rich.table import Column, Table from rich.tree import Tree -from typing_extensions import Annotated +from typing import Annotated from yaml import safe_dump, safe_load from bittensor_cli.src import ( From be6d15ee6f4ef8864681d7e50efc8a2e95b8b6c5 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 24 Feb 2026 12:34:37 -0800 Subject: [PATCH 3/3] trigger commit