From 90ecc6008119712d5b01ea654ddbd320e35c48f8 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 26 Jun 2025 18:01:47 -0700 Subject: [PATCH 01/48] add liquidity exirinsics --- bittensor/core/extrinsics/liquidity.py | 170 +++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 bittensor/core/extrinsics/liquidity.py diff --git a/bittensor/core/extrinsics/liquidity.py b/bittensor/core/extrinsics/liquidity.py new file mode 100644 index 0000000000..4e65695e32 --- /dev/null +++ b/bittensor/core/extrinsics/liquidity.py @@ -0,0 +1,170 @@ +from typing import Optional, TYPE_CHECKING + +from bittensor.utils import unlock_key +from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging +from bittensor.utils.liquidity import price_to_tick + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.subtensor import Subtensor + + +def add_liquidity_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + liquidity: Balance, + price_low: float, + price_high: float, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + period: Optional[int] = None, +) -> tuple[bool, str]: + """ + 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). + netuid: The UID of the target subnet for which the call is being initiated. + 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. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. + + 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. + + 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: + logging.error(unlock.message) + return False, unlock.message + + tick_low = price_to_tick(price_low) + tick_high = price_to_tick(price_high) + + call = subtensor.substrate.compose_call( + call_module="Swap", + call_function="add_liquidity", + call_params={ + "hotkey": wallet.hotkey.ss58_address, + "netuid": netuid, + "tick_low": tick_low, + "tick_high": tick_high, + "liquidity": liquidity, + }, + ) + + return subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + use_nonce=True, + period=period, + ) + + +def remove_liquidity_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + position_id: int, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + period: Optional[int] = None, +) -> tuple[bool, str]: + """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). + 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. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. + + 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. + + Note: Adding is allowed even when user liquidity is enabled in specified subnet. + Call `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. + """ + call = subtensor.substrate.compose_call( + call_module="Swap", + call_function="remove_liquidity", + call_params={ + "hotkey": wallet.hotkey.ss58_address, + "netuid": netuid, + "position_id": position_id, + }, + ) + + return subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + use_nonce=True, + period=period, + ) + + +def toggle_user_liquidity_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + enable: bool, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + period: Optional[int] = None, +) -> tuple[bool, str]: + """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. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. + + 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: + logging.error(unlock.message) + return False, unlock.message + + call = subtensor.substrate.compose_call( + call_module="Swap", + call_function="toggle_user_liquidity", + call_params={"netuid": netuid, "enable": enable}, + ) + + return subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + ) From c006ccf5f16ae79bf4d219d1eec1ef0e774ec93d Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 26 Jun 2025 18:02:00 -0700 Subject: [PATCH 02/48] add liquidity utils --- bittensor/utils/liquidity.py | 121 +++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 bittensor/utils/liquidity.py diff --git a/bittensor/utils/liquidity.py b/bittensor/utils/liquidity.py new file mode 100644 index 0000000000..02365731f7 --- /dev/null +++ b/bittensor/utils/liquidity.py @@ -0,0 +1,121 @@ +import math +from typing import Any + +from bittensor.utils.balance import Balance, fixed_to_float + +MIN_TICK = -887272 +MAX_TICK = 887272 +PRICE_STEP = 1.0001 + + +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}") + + log_base = math.log1p(PRICE_STEP - 1) # safer for small deltas + tick = round(math.log(price) / log_base) + + if tick < MIN_TICK or 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_above( + current_tick: int, + tick: dict, + tick_index: int, + quote: bool, + global_fees_tao: float, + global_fees_alpha: float, +) -> float: + """Returns the upper value of the liquidity fee.""" + if tick_index <= current_tick: + if quote: + return global_fees_tao - fixed_to_float(tick.get("fees_out_tao")) + else: + return global_fees_alpha - fixed_to_float(tick.get("fees_out_alpha")) + elif quote: + return fixed_to_float(tick.get("fees_out_tao")) + else: + return fixed_to_float(tick.get("fees_out_alpha")) + + +def get_fees_below( + current_tick: int, + tick: dict, + tick_index: int, + quote: bool, + global_fees_tao: float, + global_fees_alpha: float, +) -> float: + """Returns the upper value of the liquidity fee.""" + if tick_index <= current_tick: + if quote: + return fixed_to_float(tick.get("fees_out_tao")) + else: + return fixed_to_float(tick.get("fees_out_alpha")) + elif quote: + return global_fees_tao - fixed_to_float(tick.get("fees_out_tao")) + else: + return global_fees_alpha - fixed_to_float(tick.get("fees_out_alpha")) + + +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, +) -> list[Balance]: + """Calculate fees from position and fees values.""" + 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(fee_tao), Balance.from_rao(fee_alpha, netuid)] From 25f3f54daf7b671052327da09dc2e10c3954f96f Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 26 Jun 2025 18:05:48 -0700 Subject: [PATCH 03/48] add liquidity utils --- bittensor/utils/liquidity.py | 38 +++++++++--------------------------- 1 file changed, 9 insertions(+), 29 deletions(-) diff --git a/bittensor/utils/liquidity.py b/bittensor/utils/liquidity.py index 02365731f7..ebbacedefa 100644 --- a/bittensor/utils/liquidity.py +++ b/bittensor/utils/liquidity.py @@ -31,44 +31,24 @@ def tick_to_price(tick: int) -> float: return PRICE_STEP**tick -def get_fees_above( +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 upper value of the liquidity fee.""" - if tick_index <= current_tick: - if quote: - return global_fees_tao - fixed_to_float(tick.get("fees_out_tao")) - else: - return global_fees_alpha - fixed_to_float(tick.get("fees_out_alpha")) - elif quote: - return fixed_to_float(tick.get("fees_out_tao")) - else: - return fixed_to_float(tick.get("fees_out_alpha")) - + """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 -def get_fees_below( - current_tick: int, - tick: dict, - tick_index: int, - quote: bool, - global_fees_tao: float, - global_fees_alpha: float, -) -> float: - """Returns the upper value of the liquidity fee.""" - if tick_index <= current_tick: - if quote: - return fixed_to_float(tick.get("fees_out_tao")) - else: - return fixed_to_float(tick.get("fees_out_alpha")) - elif quote: - return global_fees_tao - fixed_to_float(tick.get("fees_out_tao")) + if above: + return global_fee_value - tick_fee_value if tick_index <= current_tick else tick_fee_value else: - return global_fees_alpha - fixed_to_float(tick.get("fees_out_alpha")) + return tick_fee_value if tick_index <= current_tick else global_fee_value - tick_fee_value def get_fees_in_range( From 5822962eabb21e6060026da7b0401992ca4f0719 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 26 Jun 2025 18:06:08 -0700 Subject: [PATCH 04/48] add subtensor calls --- bittensor/core/subtensor.py | 292 +++++++++++++++++++++++++++++++++++- 1 file changed, 286 insertions(+), 6 deletions(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index be9b18e9f4..3e3b9f3fa2 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -45,6 +45,11 @@ commit_weights_extrinsic, reveal_weights_extrinsic, ) +from bittensor.core.extrinsics.liquidity import ( + add_liquidity_extrinsic, + remove_liquidity_extrinsic, + toggle_user_liquidity_extrinsic, +) from bittensor.core.extrinsics.move_stake import ( transfer_stake_extrinsic, swap_stake_extrinsic, @@ -103,6 +108,12 @@ check_and_convert_to_balance, ) from bittensor.utils.btlogging import logging +from bittensor.utils.liquidity import ( + calculate_fees, + get_fees, + tick_to_price, + price_to_tick, +) from bittensor.utils.weight_utils import generate_weight_hash, convert_uids_and_weights if TYPE_CHECKING: @@ -1368,6 +1379,147 @@ def get_all_neuron_certificates( output[decode_account_id(key)] = Certificate(item.value) return output + def get_liquidity_list( + self, + wallet: "Wallet", + netuid: int, + block: Optional[int] = None, + ) -> Optional[list[dict[str, Any]]]: + """ + Retrieves all liquidity positions for the given wallet on a specified subnet (netuid). + Calculates associated fee rewards based on current global and tick-level fee data. + + Args: + wallet (Wallet): Wallet instance to fetch positions for. + netuid (int): Subnet identifier. + block: The blockchain block number for the query. + + Returns: + Optional[list[LiquidityPosition]]: List of liquidity positions, or None if subnet does not exist. + """ + if not self.subnet_exists(netuid): + return None + + query = self.substrate.query + block_hash = self.determine_block_hash(block) + + # Fetch global fees and current price + fee_global_tao = fixed_to_float( + query( + module="Swap", + storage_function="FeeGlobalTao", + params=[netuid], + block_hash=block_hash, + ) + ) + fee_global_alpha = fixed_to_float( + query( + module="Swap", + storage_function="FeeGlobalAlpha", + params=[netuid], + block_hash=block_hash, + ) + ) + sqrt_price = fixed_to_float( + query( + module="Swap", + storage_function="AlphaSqrtPrice", + params=[netuid], + block_hash=block_hash, + ) + ) + current_tick = price_to_tick(sqrt_price**2) + + # Fetch positions + positions_response = self.query_map( + module="Swap", + name="Positions", + block=block, + params=[netuid, wallet.coldkeypub.ss58_address], + ) + + positions = [] + for _, p in positions_response: + value = p.value + tick_low_idx = value["tick_low"][0] + tick_high_idx = value["tick_high"][0] + + tick_low = query( + module="Swap", + storage_function="Ticks", + params=[netuid, tick_low_idx], + block_hash=block_hash, + ) + tick_high = query( + module="Swap", + storage_function="Ticks", + params=[netuid, tick_high_idx], + block_hash=block_hash, + ) + + # Calculate fees above/below range for both tokens + tao_below = get_fees( + current_tick=current_tick, + tick=tick_low, + tick_index=tick_low_idx, + quote=True, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + above=False, + ) + tao_above = get_fees( + current_tick=current_tick, + tick=tick_high, + tick_index=tick_high_idx, + quote=True, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + above=True, + ) + alpha_below = get_fees( + current_tick=current_tick, + tick=tick_low, + tick_index=tick_low_idx, + quote=False, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + above=False, + ) + alpha_above = get_fees( + current_tick=current_tick, + tick=tick_high, + tick_index=tick_high_idx, + quote=False, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + above=True, + ) + + # Calculate fees earned by position + fees_tao, fees_alpha = calculate_fees( + position=value, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + tao_fees_below_low=tao_below, + tao_fees_above_high=tao_above, + alpha_fees_below_low=alpha_below, + alpha_fees_above_high=alpha_above, + netuid=netuid, + ) + + positions.append( + { + "id": p.value.get("id")[0], + "price_low": tick_to_price(p.value.get("tick_low")[0]), + "price_high": tick_to_price(p.value.get("tick_high")[0]), + "liquidity": Balance.from_rao(p.value.get("liquidity")), + "fees_tao": fees_tao, + "fees_alpha": fees_alpha, + } + ) + + return positions + def get_neuron_for_pubkey_and_subnet( self, hotkey_ss58: str, netuid: int, block: Optional[int] = None ) -> Optional["NeuronInfo"]: @@ -2300,11 +2452,14 @@ def query_identity( See the `Bittensor CLI documentation `_ for supported identity parameters. """ - identity_info = self.substrate.query( - module="SubtensorModule", - storage_function="IdentitiesV2", - params=[coldkey_ss58], - block_hash=self.determine_block_hash(block), + identity_info = cast( + dict, + self.substrate.query( + module="SubtensorModule", + storage_function="IdentitiesV2", + params=[coldkey_ss58], + block_hash=self.determine_block_hash(block), + ), ) if not identity_info: @@ -2760,6 +2915,52 @@ def add_stake( period=period, ) + def add_liquidity( + self, + wallet: "Wallet", + netuid: int, + liquidity: Balance, + price_low: float, + price_high: float, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + period: Optional[int] = None, + ) -> tuple[bool, str]: + """ + Adds liquidity to the specified price range. + + Arguments: + 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. + 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. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. + + 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. + + Note: Adding is allowed even when user liquidity is enabled in specified subnet. Call `toggle_user_liquidity` + method to enable/disable user liquidity. + """ + return add_liquidity_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + liquidity=liquidity, + price_low=price_low, + price_high=price_high, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + ) + def add_stake_multiple( self, wallet: "Wallet", @@ -3067,6 +3268,47 @@ def register_subnet( period=period, ) + def remove_liquidity( + self, + wallet: "Wallet", + netuid: int, + position_id: int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, + period: Optional[int] = None, + ) -> tuple[bool, str]: + """Remove liquidity and credit balances back to wallet's hotkey stake. + + Arguments: + 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. + 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. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. + + 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. + + Note: + - Adding is allowed even when user liquidity is enabled in specified subnet. Call `toggle_user_liquidity` + extrinsic to enable/disable user liquidity. + - To get the `position_id` use `get_liquidity_list` method. + """ + return remove_liquidity_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + position_id=position_id, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + ) + def reveal_weights( self, wallet: "Wallet", @@ -3471,7 +3713,7 @@ def _blocks_weight_limit() -> bool: f"Hotkey {wallet.hotkey.ss58_address} not registered in subnet {netuid}", ) - if self.commit_reveal_enabled(netuid=netuid) is True: + if self.commit_reveal_enabled(netuid=netuid): # go with `commit reveal v3` extrinsic while retries < max_retries and success is False and _blocks_weight_limit(): @@ -3662,6 +3904,44 @@ def swap_stake( period=period, ) + def toggle_user_liquidity( + self, + wallet: "Wallet", + netuid: int, + enable: bool, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + period: Optional[int] = None, + ) -> tuple[bool, str]: + """Allow to toggle user liquidity for specified subnet. + + Arguments: + 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. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. + + 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. + + Note: The call can be executed successfully by the subnet owner only. + """ + return toggle_user_liquidity_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + enable=enable, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + ) + def transfer( self, wallet: "Wallet", From d64701abe6871760ef558a66aecdfa66789b4142 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 26 Jun 2025 18:10:36 -0700 Subject: [PATCH 05/48] ruff --- bittensor/utils/liquidity.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/bittensor/utils/liquidity.py b/bittensor/utils/liquidity.py index ebbacedefa..9e37ef9b1f 100644 --- a/bittensor/utils/liquidity.py +++ b/bittensor/utils/liquidity.py @@ -41,14 +41,22 @@ def get_fees( above: bool, ) -> float: """Returns the liquidity fee.""" - tick_fee_key = 'fees_out_tao' if quote else 'fees_out_alpha' + 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 ( + global_fee_value - tick_fee_value + if tick_index <= current_tick + else tick_fee_value + ) else: - return tick_fee_value if tick_index <= current_tick else global_fee_value - tick_fee_value + return ( + tick_fee_value + if tick_index <= current_tick + else global_fee_value - tick_fee_value + ) def get_fees_in_range( From 26e4bd0715ffccd025350a5c6e202aa4633ad074 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 26 Jun 2025 18:18:11 -0700 Subject: [PATCH 06/48] docstring --- bittensor/core/subtensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 3e3b9f3fa2..50f182d7de 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1390,12 +1390,12 @@ def get_liquidity_list( Calculates associated fee rewards based on current global and tick-level fee data. Args: - wallet (Wallet): Wallet instance to fetch positions for. - netuid (int): Subnet identifier. + wallet: Wallet instance to fetch positions for. + netuid: Subnet unique id. block: The blockchain block number for the query. Returns: - Optional[list[LiquidityPosition]]: List of liquidity positions, or None if subnet does not exist. + List of liquidity positions, or None if subnet does not exist. """ if not self.subnet_exists(netuid): return None From ce748e7348d4f11897a6d3c39277a2999e9cef17 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 26 Jun 2025 18:32:13 -0700 Subject: [PATCH 07/48] SubtensorApi --- bittensor/core/subtensor_api/extrinsics.py | 3 +++ bittensor/core/subtensor_api/subnets.py | 1 + bittensor/core/subtensor_api/utils.py | 4 ++++ 3 files changed, 8 insertions(+) diff --git a/bittensor/core/subtensor_api/extrinsics.py b/bittensor/core/subtensor_api/extrinsics.py index 0096fe8c8c..a133540465 100644 --- a/bittensor/core/subtensor_api/extrinsics.py +++ b/bittensor/core/subtensor_api/extrinsics.py @@ -7,6 +7,7 @@ class Extrinsics: """Class for managing extrinsic operations.""" def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): + self.add_liquidity = subtensor.add_liquidity self.add_stake = subtensor.add_stake self.add_stake_multiple = subtensor.add_stake_multiple self.burned_register = subtensor.burned_register @@ -14,6 +15,7 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.move_stake = subtensor.move_stake self.register = subtensor.register self.register_subnet = subtensor.register_subnet + self.remove_liquidity = subtensor.remove_liquidity self.reveal_weights = subtensor.reveal_weights self.root_register = subtensor.root_register self.root_set_weights = subtensor.root_set_weights @@ -26,6 +28,7 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.serve_axon = subtensor.serve_axon self.start_call = subtensor.start_call self.swap_stake = subtensor.swap_stake + self.toggle_user_liquidity = subtensor.toggle_user_liquidity self.transfer = subtensor.transfer self.transfer_stake = subtensor.transfer_stake self.unstake = subtensor.unstake diff --git a/bittensor/core/subtensor_api/subnets.py b/bittensor/core/subtensor_api/subnets.py index ddeedaf1fa..c5eb782164 100644 --- a/bittensor/core/subtensor_api/subnets.py +++ b/bittensor/core/subtensor_api/subnets.py @@ -19,6 +19,7 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.get_children_pending = subtensor.get_children_pending self.get_current_weight_commit_info = subtensor.get_current_weight_commit_info self.get_hyperparameter = subtensor.get_hyperparameter + self.get_liquidity_list = subtensor.get_liquidity_list self.get_neuron_for_pubkey_and_subnet = ( subtensor.get_neuron_for_pubkey_and_subnet ) diff --git a/bittensor/core/subtensor_api/utils.py b/bittensor/core/subtensor_api/utils.py index f0f2ded013..791c28e490 100644 --- a/bittensor/core/subtensor_api/utils.py +++ b/bittensor/core/subtensor_api/utils.py @@ -6,6 +6,7 @@ def add_legacy_methods(subtensor: "SubtensorApi"): """If SubtensorApi get `subtensor_fields=True` arguments, then all classic Subtensor fields added to root level.""" + subtensor.add_liquidity = subtensor._subtensor.add_liquidity subtensor.add_stake = subtensor._subtensor.add_stake subtensor.add_stake_multiple = subtensor._subtensor.add_stake_multiple subtensor.all_subnets = subtensor._subtensor.all_subnets @@ -53,6 +54,7 @@ def add_legacy_methods(subtensor: "SubtensorApi"): subtensor.get_hotkey_owner = subtensor._subtensor.get_hotkey_owner subtensor.get_hotkey_stake = subtensor._subtensor.get_hotkey_stake subtensor.get_hyperparameter = subtensor._subtensor.get_hyperparameter + subtensor.get_liquidity_list = subtensor._subtensor.get_liquidity_list subtensor.get_metagraph_info = subtensor._subtensor.get_metagraph_info subtensor.get_minimum_required_stake = ( subtensor._subtensor.get_minimum_required_stake @@ -129,6 +131,7 @@ def add_legacy_methods(subtensor: "SubtensorApi"): subtensor.query_runtime_api = subtensor._subtensor.query_runtime_api subtensor.query_subtensor = subtensor._subtensor.query_subtensor subtensor.recycle = subtensor._subtensor.recycle + subtensor.remove_liquidity = subtensor._subtensor.remove_liquidity subtensor.register = subtensor._subtensor.register subtensor.register_subnet = subtensor._subtensor.register_subnet subtensor.reveal_weights = subtensor._subtensor.reveal_weights @@ -154,6 +157,7 @@ def add_legacy_methods(subtensor: "SubtensorApi"): subtensor.substrate = subtensor._subtensor.substrate subtensor.swap_stake = subtensor._subtensor.swap_stake subtensor.tempo = subtensor._subtensor.tempo + subtensor.toggle_user_liquidity = subtensor._subtensor.toggle_user_liquidity subtensor.transfer = subtensor._subtensor.transfer subtensor.transfer_stake = subtensor._subtensor.transfer_stake subtensor.tx_rate_limit = subtensor._subtensor.tx_rate_limit From 04652ec2b2c489cd5f5fdab56c54d448aac42e62 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 26 Jun 2025 18:32:35 -0700 Subject: [PATCH 08/48] fix tests with empty methods --- bittensor/core/async_subtensor.py | 43 +++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index d0ca2de7ac..3ef2eb8199 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -1781,6 +1781,14 @@ async def get_all_neuron_certificates( output[decode_account_id(key)] = Certificate(item.value) return output + async def get_liquidity_list( + self, + wallet: "Wallet", + netuid: int, + block: Optional[int] = None, + ) -> Optional[list[dict[str, Any]]]: + return None + async def get_neuron_for_pubkey_and_subnet( self, hotkey_ss58: str, @@ -3532,6 +3540,19 @@ async def add_stake( period=period, ) + async def add_liquidity( + self, + wallet: "Wallet", + netuid: int, + liquidity: Balance, + price_low: float, + price_high: float, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + period: Optional[int] = None, + ) -> tuple[bool, str]: + return True, "" + async def add_stake_multiple( self, wallet: "Wallet", @@ -3835,6 +3856,17 @@ async def register_subnet( period=period, ) + async def remove_liquidity( + self, + wallet: "Wallet", + netuid: int, + position_id: int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, + period: Optional[int] = None, + ) -> tuple[bool, str]: + return True, "" + async def reveal_weights( self, wallet: "Wallet", @@ -4459,6 +4491,17 @@ async def swap_stake( period=period, ) + async def toggle_user_liquidity( + self, + wallet: "Wallet", + netuid: int, + enable: bool, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + period: Optional[int] = None, + ) -> tuple[bool, str]: + return True, "" + async def transfer( self, wallet: "Wallet", From 1e3c2792325de605c219f82b10e8cc27e8ae50c8 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 30 Jun 2025 10:57:42 -0700 Subject: [PATCH 09/48] add async extrinsics --- .../core/extrinsics/asyncex/liquidity.py | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 bittensor/core/extrinsics/asyncex/liquidity.py diff --git a/bittensor/core/extrinsics/asyncex/liquidity.py b/bittensor/core/extrinsics/asyncex/liquidity.py new file mode 100644 index 0000000000..6fddea9fe2 --- /dev/null +++ b/bittensor/core/extrinsics/asyncex/liquidity.py @@ -0,0 +1,174 @@ +from typing import Optional, TYPE_CHECKING + +from bittensor.utils import unlock_key +from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging +from bittensor.utils.liquidity import price_to_tick + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.async_subtensor import AsyncSubtensor + + +async def add_liquidity_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + liquidity: Balance, + price_low: float, + price_high: float, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + period: Optional[int] = None, +) -> tuple[bool, str]: + """ + 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). + netuid: The UID of the target subnet for which the call is being initiated. + 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. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. + + 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. + + 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: + logging.error(unlock.message) + return False, unlock.message + + tick_low = price_to_tick(price_low) + tick_high = price_to_tick(price_high) + + call = await subtensor.substrate.compose_call( + call_module="Swap", + call_function="add_liquidity", + call_params={ + "hotkey": wallet.hotkey.ss58_address, + "netuid": netuid, + "tick_low": tick_low, + "tick_high": tick_high, + "liquidity": liquidity, + }, + ) + + return await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + use_nonce=True, + period=period, + ) + + +async def remove_liquidity_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + position_id: int, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + period: Optional[int] = None, +) -> tuple[bool, str]: + """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). + 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. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. + + 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. + + 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: + logging.error(unlock.message) + return False, unlock.message + + call = await subtensor.substrate.compose_call( + call_module="Swap", + call_function="remove_liquidity", + call_params={ + "hotkey": wallet.hotkey.ss58_address, + "netuid": netuid, + "position_id": position_id, + }, + ) + + return await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + use_nonce=True, + period=period, + ) + + +async def toggle_user_liquidity_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + enable: bool, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + period: Optional[int] = None, +) -> tuple[bool, str]: + """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. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. + + 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: + logging.error(unlock.message) + return False, unlock.message + + 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, + period=period, + ) From e1824e62ecaffe46aa315e1e0b06d81cee22c665 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 30 Jun 2025 10:58:00 -0700 Subject: [PATCH 10/48] update remove_liquidity_extrinsic --- bittensor/core/extrinsics/liquidity.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bittensor/core/extrinsics/liquidity.py b/bittensor/core/extrinsics/liquidity.py index 4e65695e32..5fe6cb3f62 100644 --- a/bittensor/core/extrinsics/liquidity.py +++ b/bittensor/core/extrinsics/liquidity.py @@ -104,6 +104,10 @@ def remove_liquidity_extrinsic( 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: + logging.error(unlock.message) + return False, unlock.message + call = subtensor.substrate.compose_call( call_module="Swap", call_function="remove_liquidity", From b33e63a2c00862f5277ff9d200704c69b96198d6 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 30 Jun 2025 10:58:10 -0700 Subject: [PATCH 11/48] add async subtensor calls --- bittensor/core/async_subtensor.py | 258 +++++++++++++++++++++++++++++- 1 file changed, 250 insertions(+), 8 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 910749d26b..3a05a0c2eb 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -39,16 +39,16 @@ ) from bittensor.core.config import Config from bittensor.core.errors import ChainError, SubstrateRequestException +from bittensor.core.extrinsics.asyncex.children import ( + root_set_pending_childkey_cooldown_extrinsic, + set_children_extrinsic, +) from bittensor.core.extrinsics.asyncex.commit_reveal import commit_reveal_v3_extrinsic from bittensor.core.extrinsics.asyncex.move_stake import ( transfer_stake_extrinsic, swap_stake_extrinsic, move_stake_extrinsic, ) -from bittensor.core.extrinsics.asyncex.children import ( - root_set_pending_childkey_cooldown_extrinsic, - set_children_extrinsic, -) from bittensor.core.extrinsics.asyncex.registration import ( burned_register_extrinsic, register_extrinsic, @@ -96,12 +96,23 @@ u16_normalized_float, u64_normalized_float, ) +from bittensor.core.extrinsics.asyncex.liquidity import ( + add_liquidity_extrinsic, + remove_liquidity_extrinsic, + toggle_user_liquidity_extrinsic, +) from bittensor.utils.balance import ( Balance, fixed_to_float, check_and_convert_to_balance, ) from bittensor.utils.btlogging import logging +from bittensor.utils.liquidity import ( + calculate_fees, + get_fees, + tick_to_price, + price_to_tick, +) from bittensor.utils.weight_utils import generate_weight_hash, convert_uids_and_weights if TYPE_CHECKING: @@ -1816,8 +1827,149 @@ async def get_liquidity_list( wallet: "Wallet", netuid: int, block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, ) -> Optional[list[dict[str, Any]]]: - return None + """ + Retrieves all liquidity positions for the given wallet on a specified subnet (netuid). + Calculates associated fee rewards based on current global and tick-level fee data. + + Args: + wallet: Wallet instance to fetch positions for. + netuid: Subnet unique id. + block: The blockchain block number for the query. + block_hash: The hash of the block to retrieve the parameter from. Do not specify if using block or + reuse_block. + reuse_block: Whether to use the last-used block. Do not set if using block_hash or block. + + Returns: + List of liquidity positions, or None if subnet does not exist. + """ + if not await self.subnet_exists(netuid): + return None + block_hash = await self.determine_block_hash( + block=block, block_hash=block_hash, reuse_block=reuse_block + ) + + query = self.substrate.query + ( + fee_global_tao, + fee_global_alpha, + sqrt_price, + positions_response, + ) = await asyncio.gather( + query( + module="Swap", + storage_function="FeeGlobalTao", + params=[netuid], + block_hash=block_hash, + ), + query( + module="Swap", + storage_function="FeeGlobalAlpha", + params=[netuid], + block_hash=block_hash, + ), + query( + module="Swap", + storage_function="AlphaSqrtPrice", + params=[netuid], + block_hash=block_hash, + ), + self.query_map( + module="Swap", + name="Positions", + block=block, + params=[netuid, wallet.coldkeypub.ss58_address], + ), + ) + # Fetch global fees and current price + current_tick = price_to_tick(sqrt_price**2) + + # Fetch positions + positions = [] + for _, p in positions_response: + value = p.value + tick_low_idx = value["tick_low"][0] + tick_high_idx = value["tick_high"][0] + + tick_low, tick_high = await asyncio.gather( + query( + module="Swap", + storage_function="Ticks", + params=[netuid, tick_low_idx], + block_hash=block_hash, + ), + query( + module="Swap", + storage_function="Ticks", + params=[netuid, tick_high_idx], + block_hash=block_hash, + ), + ) + + # Calculate fees above/below range for both tokens + tao_below = get_fees( + current_tick=current_tick, + tick=tick_low, + tick_index=tick_low_idx, + quote=True, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + above=False, + ) + tao_above = get_fees( + current_tick=current_tick, + tick=tick_high, + tick_index=tick_high_idx, + quote=True, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + above=True, + ) + alpha_below = get_fees( + current_tick=current_tick, + tick=tick_low, + tick_index=tick_low_idx, + quote=False, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + above=False, + ) + alpha_above = get_fees( + current_tick=current_tick, + tick=tick_high, + tick_index=tick_high_idx, + quote=False, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + above=True, + ) + + # Calculate fees earned by position + fees_tao, fees_alpha = calculate_fees( + position=value, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + tao_fees_below_low=tao_below, + tao_fees_above_high=tao_above, + alpha_fees_below_low=alpha_below, + alpha_fees_above_high=alpha_above, + netuid=netuid, + ) + + positions.append( + { + "id": p.value.get("id")[0], + "price_low": tick_to_price(p.value.get("tick_low")[0]), + "price_high": tick_to_price(p.value.get("tick_high")[0]), + "liquidity": Balance.from_rao(p.value.get("liquidity")), + "fees_tao": fees_tao, + "fees_alpha": fees_alpha, + } + ) + + return positions async def get_neuron_for_pubkey_and_subnet( self, @@ -3581,7 +3733,40 @@ async def add_liquidity( wait_for_finalization: bool = False, period: Optional[int] = None, ) -> tuple[bool, str]: - return True, "" + """ + Adds liquidity to the specified price range. + + Arguments: + 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. + 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. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. + + 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. + + Note: Adding is allowed even when user liquidity is enabled in specified subnet. Call `toggle_user_liquidity` + method to enable/disable user liquidity. + """ + return await add_liquidity_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + liquidity=liquidity, + price_low=price_low, + price_high=price_high, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + ) async def add_stake_multiple( self, @@ -3895,7 +4080,37 @@ async def remove_liquidity( wait_for_finalization: bool = True, period: Optional[int] = None, ) -> tuple[bool, str]: - return True, "" + """Remove liquidity and credit balances back to wallet's hotkey stake. + + Arguments: + 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. + 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. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. + + 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. + + Note: + - Adding is allowed even when user liquidity is enabled in specified subnet. Call `toggle_user_liquidity` + extrinsic to enable/disable user liquidity. + - To get the `position_id` use `get_liquidity_list` method. + """ + return await remove_liquidity_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + position_id=position_id, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + ) async def reveal_weights( self, @@ -4530,7 +4745,34 @@ async def toggle_user_liquidity( wait_for_finalization: bool = False, period: Optional[int] = None, ) -> tuple[bool, str]: - return True, "" + """Allow to toggle user liquidity for specified subnet. + + Arguments: + 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. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. + + 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. + + Note: The call can be executed successfully by the subnet owner only. + """ + return await toggle_user_liquidity_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + enable=enable, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + ) async def transfer( self, From 955e7ac5103217f2e271778ac2df2fd305ec545f Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 30 Jun 2025 16:23:27 -0700 Subject: [PATCH 12/48] improve `bittensor/utils/liquidity.py` --- bittensor/utils/liquidity.py | 59 +++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/bittensor/utils/liquidity.py b/bittensor/utils/liquidity.py index 9e37ef9b1f..e79623b0d8 100644 --- a/bittensor/utils/liquidity.py +++ b/bittensor/utils/liquidity.py @@ -1,13 +1,60 @@ +""" +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 typing import Any +from dataclasses import dataclass from bittensor.utils.balance import Balance, fixed_to_float +# 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 + + def to_token_amounts(self, current_subnet_price: Balance, netuid: int) -> tuple[Balance, Balance]: + """Convert a position to token amounts. + + Arguments: + current_subnet_price: current subnet price in Alpha. + netuid: Subnet uid. + + 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), 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: @@ -51,12 +98,11 @@ def get_fees( if tick_index <= current_tick else tick_fee_value ) - else: - return ( - tick_fee_value - if tick_index <= current_tick - else global_fee_value - tick_fee_value - ) + return ( + tick_fee_value + if tick_index <= current_tick + else global_fee_value - tick_fee_value + ) def get_fees_in_range( @@ -100,7 +146,6 @@ def calculate_fees( 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 From 5d96b1296e4c54ab709135895cd40e458e758b1a Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 30 Jun 2025 16:29:27 -0700 Subject: [PATCH 13/48] ruff --- bittensor/utils/liquidity.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/bittensor/utils/liquidity.py b/bittensor/utils/liquidity.py index e79623b0d8..500b190895 100644 --- a/bittensor/utils/liquidity.py +++ b/bittensor/utils/liquidity.py @@ -25,7 +25,9 @@ class LiquidityPosition: fees_tao: Balance # RAO fees_alpha: Balance # RAO - def to_token_amounts(self, current_subnet_price: Balance, netuid: int) -> tuple[Balance, Balance]: + def to_token_amounts( + self, current_subnet_price: Balance, netuid: int + ) -> tuple[Balance, Balance]: """Convert a position to token amounts. Arguments: @@ -50,9 +52,13 @@ def to_token_amounts(self, current_subnet_price: Balance, netuid: int) -> tuple[ 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_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), netuid), Balance.from_rao(int(amount_tao)) + return Balance.from_rao(int(amount_alpha), netuid), Balance.from_rao( + int(amount_tao) + ) def price_to_tick(price: float) -> int: From b5fa2701e27e6e1022db3b5894807ee5c8f14974 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 30 Jun 2025 16:29:42 -0700 Subject: [PATCH 14/48] update `get_liquidity_list` (use LiquidityPosition) --- bittensor/core/async_subtensor.py | 25 ++++++++++++++++--------- bittensor/core/subtensor.py | 25 ++++++++++++++++--------- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 3a05a0c2eb..1fc783c40e 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -112,6 +112,7 @@ get_fees, tick_to_price, price_to_tick, + LiquidityPosition, ) from bittensor.utils.weight_utils import generate_weight_hash, convert_uids_and_weights @@ -1829,7 +1830,7 @@ async def get_liquidity_list( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> Optional[list[dict[str, Any]]]: + ) -> Optional[list[LiquidityPosition]]: """ Retrieves all liquidity positions for the given wallet on a specified subnet (netuid). Calculates associated fee rewards based on current global and tick-level fee data. @@ -1959,14 +1960,20 @@ async def get_liquidity_list( ) positions.append( - { - "id": p.value.get("id")[0], - "price_low": tick_to_price(p.value.get("tick_low")[0]), - "price_high": tick_to_price(p.value.get("tick_high")[0]), - "liquidity": Balance.from_rao(p.value.get("liquidity")), - "fees_tao": fees_tao, - "fees_alpha": fees_alpha, - } + LiquidityPosition( + **{ + "id": p.value.get("id")[0], + "price_low": Balance.from_rao( + int(tick_to_price(p.value.get("tick_low")[0])) + ), + "price_high": Balance.from_rao( + int(tick_to_price(p.value.get("tick_high")[0])) + ), + "liquidity": Balance.from_rao(p.value.get("liquidity")), + "fees_tao": fees_tao, + "fees_alpha": fees_alpha, + } + ) ) return positions diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 84cba00766..668096fcc2 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -115,6 +115,7 @@ get_fees, tick_to_price, price_to_tick, + LiquidityPosition, ) from bittensor.utils.weight_utils import generate_weight_hash, convert_uids_and_weights @@ -1413,7 +1414,7 @@ def get_liquidity_list( wallet: "Wallet", netuid: int, block: Optional[int] = None, - ) -> Optional[list[dict[str, Any]]]: + ) -> Optional[list[LiquidityPosition]]: """ Retrieves all liquidity positions for the given wallet on a specified subnet (netuid). Calculates associated fee rewards based on current global and tick-level fee data. @@ -1537,14 +1538,20 @@ def get_liquidity_list( ) positions.append( - { - "id": p.value.get("id")[0], - "price_low": tick_to_price(p.value.get("tick_low")[0]), - "price_high": tick_to_price(p.value.get("tick_high")[0]), - "liquidity": Balance.from_rao(p.value.get("liquidity")), - "fees_tao": fees_tao, - "fees_alpha": fees_alpha, - } + LiquidityPosition( + **{ + "id": p.value.get("id")[0], + "price_low": Balance.from_rao( + int(tick_to_price(p.value.get("tick_low")[0])) + ), + "price_high": Balance.from_rao( + int(tick_to_price(p.value.get("tick_high")[0])) + ), + "liquidity": Balance.from_rao(p.value.get("liquidity")), + "fees_tao": fees_tao, + "fees_alpha": fees_alpha, + } + ) ) return positions From 26c5ac79a6f90db5d6c36cfb1e3dafbefb25f28f Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 30 Jun 2025 16:39:18 -0700 Subject: [PATCH 15/48] add `modify_liquidity_extrinsic` --- .../core/extrinsics/asyncex/liquidity.py | 57 +++++++++++++++++++ bittensor/core/extrinsics/liquidity.py | 57 +++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/bittensor/core/extrinsics/asyncex/liquidity.py b/bittensor/core/extrinsics/asyncex/liquidity.py index 6fddea9fe2..67abe674e3 100644 --- a/bittensor/core/extrinsics/asyncex/liquidity.py +++ b/bittensor/core/extrinsics/asyncex/liquidity.py @@ -74,6 +74,63 @@ async def add_liquidity_extrinsic( ) +async def modify_liquidity_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + position_id: int, + liquidity_delta: Balance, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + period: Optional[int] = None, +) -> tuple[bool, str]: + """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). + 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. + 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. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. + + 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. + + 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: + logging.error(unlock.message) + return False, unlock.message + + call = await subtensor.substrate.compose_call( + call_module="Swap", + call_function="modify_position", + call_params={ + "hotkey": wallet.hotkey.ss58_address, + "netuid": netuid, + "position_id": position_id, + "liquidity_delta": liquidity_delta, + }, + ) + + return await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + use_nonce=True, + period=period, + ) + + async def remove_liquidity_extrinsic( subtensor: "AsyncSubtensor", wallet: "Wallet", diff --git a/bittensor/core/extrinsics/liquidity.py b/bittensor/core/extrinsics/liquidity.py index 5fe6cb3f62..de155bb5c3 100644 --- a/bittensor/core/extrinsics/liquidity.py +++ b/bittensor/core/extrinsics/liquidity.py @@ -74,6 +74,63 @@ def add_liquidity_extrinsic( ) +def modify_liquidity_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + position_id: int, + liquidity_delta: Balance, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + period: Optional[int] = None, +) -> tuple[bool, str]: + """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). + 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. + 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. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. + + 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. + + 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: + logging.error(unlock.message) + return False, unlock.message + + call = subtensor.substrate.compose_call( + call_module="Swap", + call_function="modify_position", + call_params={ + "hotkey": wallet.hotkey.ss58_address, + "netuid": netuid, + "position_id": position_id, + "liquidity_delta": liquidity_delta, + }, + ) + + return subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + use_nonce=True, + period=period, + ) + + def remove_liquidity_extrinsic( subtensor: "Subtensor", wallet: "Wallet", From 880eb37549b41589df9284578227ff21fdc989fd Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 30 Jun 2025 17:19:42 -0700 Subject: [PATCH 16/48] ruff --- bittensor/core/extrinsics/liquidity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor/core/extrinsics/liquidity.py b/bittensor/core/extrinsics/liquidity.py index de155bb5c3..1cd832dc04 100644 --- a/bittensor/core/extrinsics/liquidity.py +++ b/bittensor/core/extrinsics/liquidity.py @@ -103,8 +103,8 @@ def modify_liquidity_extrinsic( - 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. - Note: Modifying is allowed even when user liquidity is enabled in specified subnet. - Call `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. + 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: logging.error(unlock.message) From 462ee8367c42994a1ded21d7f7779ac4a6ec5c6c Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 30 Jun 2025 17:20:09 -0700 Subject: [PATCH 17/48] add `modify_liquidity` methods --- bittensor/core/async_subtensor.py | 67 ++++++++++++++++++++++++++++++ bittensor/core/subtensor.py | 69 +++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 1fc783c40e..9950c6aa47 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -98,6 +98,7 @@ ) from bittensor.core.extrinsics.asyncex.liquidity import ( add_liquidity_extrinsic, + modify_liquidity_extrinsic, remove_liquidity_extrinsic, toggle_user_liquidity_extrinsic, ) @@ -3937,6 +3938,72 @@ async def commit_weights( return success, message + async def modify_liquidity( + self, + wallet: "Wallet", + netuid: int, + position_id: int, + liquidity_delta: Balance, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + period: Optional[int] = None, + ) -> tuple[bool, str]: + """Modifies liquidity in liquidity position by adding or removing liquidity from it. + + Arguments: + 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. + 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. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. + + 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. + + Example: + import bittensor as bt + + subtensor = bt.AsyncSubtensor(network="local") + my_wallet = bt.Wallet() + + # if `liquidity_delta` is negative + my_liquidity_delta = Balance.from_tao(100) * -1 + await subtensor.modify_liquidity( + wallet=my_wallet, + netuid=123, + position_id=2, + liquidity_delta=my_liquidity_delta + ) + + # if `liquidity_delta` is positive + my_liquidity_delta = Balance.from_tao(120) + await subtensor.modify_liquidity( + wallet=my_wallet, + netuid=123, + position_id=2, + liquidity_delta=my_liquidity_delta + ) + + Note: Modifying is allowed even when user liquidity is enabled in specified subnet. Call `toggle_user_liquidity` + to enable/disable user liquidity. + """ + return await modify_liquidity_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + position_id=position_id, + liquidity_delta=liquidity_delta, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + ) + async def move_stake( self, wallet: "Wallet", diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 668096fcc2..9794c1c1de 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -48,6 +48,7 @@ ) from bittensor.core.extrinsics.liquidity import ( add_liquidity_extrinsic, + modify_liquidity_extrinsic, remove_liquidity_extrinsic, toggle_user_liquidity_extrinsic, ) @@ -3164,6 +3165,74 @@ def commit_weights( return success, message + def modify_liquidity( + self, + wallet: "Wallet", + netuid: int, + position_id: int, + liquidity_delta: Balance, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + period: Optional[int] = None, + ) -> tuple[bool, str]: + """Modifies liquidity in liquidity position by adding or removing liquidity from it. + + Arguments: + 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. + 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. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. + + 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. + + Example: + import bittensor as bt + + subtensor = bt.subtensor(network="local") + my_wallet = bt.Wallet() + + # if `liquidity_delta` is negative + my_liquidity_delta = Balance.from_tao(100) * -1 + + subtensor.modify_liquidity( + wallet=my_wallet, + netuid=123, + position_id=2, + liquidity_delta=my_liquidity_delta + ) + + # if `liquidity_delta` is positive + my_liquidity_delta = Balance.from_tao(120) + + subtensor.modify_liquidity( + wallet=my_wallet, + netuid=123, + position_id=2, + liquidity_delta=my_liquidity_delta + ) + + Note: Modifying is allowed even when user liquidity is enabled in specified subnet. Call `toggle_user_liquidity` + to enable/disable user liquidity. + """ + return modify_liquidity_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + position_id=position_id, + liquidity_delta=liquidity_delta, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + ) + def move_stake( self, wallet: "Wallet", From 7fe8659b69b3654f20016d0352350943db0ec203 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 30 Jun 2025 17:34:56 -0700 Subject: [PATCH 18/48] update SubtensorApi --- bittensor/core/subtensor_api/extrinsics.py | 1 + bittensor/core/subtensor_api/utils.py | 1 + 2 files changed, 2 insertions(+) diff --git a/bittensor/core/subtensor_api/extrinsics.py b/bittensor/core/subtensor_api/extrinsics.py index a133540465..25bd390a79 100644 --- a/bittensor/core/subtensor_api/extrinsics.py +++ b/bittensor/core/subtensor_api/extrinsics.py @@ -12,6 +12,7 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.add_stake_multiple = subtensor.add_stake_multiple self.burned_register = subtensor.burned_register self.commit_weights = subtensor.commit_weights + self.modify_liquidity = subtensor.modify_liquidity self.move_stake = subtensor.move_stake self.register = subtensor.register self.register_subnet = subtensor.register_subnet diff --git a/bittensor/core/subtensor_api/utils.py b/bittensor/core/subtensor_api/utils.py index 4c0db212ef..0d105585ff 100644 --- a/bittensor/core/subtensor_api/utils.py +++ b/bittensor/core/subtensor_api/utils.py @@ -121,6 +121,7 @@ def add_legacy_methods(subtensor: "SubtensorApi"): subtensor.max_weight_limit = subtensor._subtensor.max_weight_limit subtensor.metagraph = subtensor._subtensor.metagraph subtensor.min_allowed_weights = subtensor._subtensor.min_allowed_weights + subtensor.modify_liquidity = subtensor._subtensor.modify_liquidity subtensor.move_stake = subtensor._subtensor.move_stake subtensor.network = subtensor._subtensor.network subtensor.neurons = subtensor._subtensor.neurons From 5996563424e9825bd860544a43e8d468810f910e Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 30 Jun 2025 17:41:25 -0700 Subject: [PATCH 19/48] improve `get_liquidity_list` methods --- bittensor/core/async_subtensor.py | 8 +++++++- bittensor/core/subtensor.py | 7 ++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 9950c6aa47..8284f2078b 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -1847,8 +1847,14 @@ async def get_liquidity_list( Returns: List of liquidity positions, or None if subnet does not exist. """ - if not await self.subnet_exists(netuid): + if not await self.subnet_exists(netuid=netuid): + logging.debug(f"Subnet {netuid} does not exist.") return None + + if not await self.is_subnet_active(netuid=netuid): + logging.debug(f"Subnet {netuid} is not active.") + return None + block_hash = await self.determine_block_hash( block=block, block_hash=block_hash, reuse_block=reuse_block ) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 9794c1c1de..c8e84fa191 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1428,7 +1428,12 @@ def get_liquidity_list( Returns: List of liquidity positions, or None if subnet does not exist. """ - if not self.subnet_exists(netuid): + if not self.subnet_exists(netuid=netuid): + logging.debug(f"Subnet {netuid} does not exist.") + return None + + if not self.is_subnet_active(netuid=netuid): + logging.debug(f"Subnet {netuid} is not active.") return None query = self.substrate.query From b0d0d13090e3f2a552e98eec3f339403c0e1957d Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 30 Jun 2025 18:14:15 -0700 Subject: [PATCH 20/48] improve `LiquidityPosition` --- bittensor/utils/liquidity.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor/utils/liquidity.py b/bittensor/utils/liquidity.py index 500b190895..6a621990b4 100644 --- a/bittensor/utils/liquidity.py +++ b/bittensor/utils/liquidity.py @@ -24,15 +24,15 @@ class LiquidityPosition: 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, netuid: int + self, current_subnet_price: Balance ) -> tuple[Balance, Balance]: """Convert a position to token amounts. Arguments: current_subnet_price: current subnet price in Alpha. - netuid: Subnet uid. Returns: tuple[int, int]: @@ -56,7 +56,7 @@ def to_token_amounts( 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), netuid), Balance.from_rao( + return Balance.from_rao(int(amount_alpha), self.netuid), Balance.from_rao( int(amount_tao) ) From 3d2f78e29498cd8ae1337c1cc742129bb6eec318 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 30 Jun 2025 18:14:33 -0700 Subject: [PATCH 21/48] improve `get_liquidity_list` method --- bittensor/core/async_subtensor.py | 17 +++++++++-------- bittensor/core/subtensor.py | 19 +++++++++++-------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 8284f2078b..8133433e8d 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -1897,9 +1897,10 @@ async def get_liquidity_list( # Fetch positions positions = [] for _, p in positions_response: - value = p.value - tick_low_idx = value["tick_low"][0] - tick_high_idx = value["tick_high"][0] + position = p.value + + tick_low_idx = position.get("tick_low")[0] + tick_high_idx = position.get("tick_high")[0] tick_low, tick_high = await asyncio.gather( query( @@ -1956,7 +1957,7 @@ async def get_liquidity_list( # Calculate fees earned by position fees_tao, fees_alpha = calculate_fees( - position=value, + position=position, global_fees_tao=fee_global_tao, global_fees_alpha=fee_global_alpha, tao_fees_below_low=tao_below, @@ -1969,14 +1970,14 @@ async def get_liquidity_list( positions.append( LiquidityPosition( **{ - "id": p.value.get("id")[0], + "id": position.get("id")[0], "price_low": Balance.from_rao( - int(tick_to_price(p.value.get("tick_low")[0])) + int(tick_to_price(position.get("tick_low")[0])) ), "price_high": Balance.from_rao( - int(tick_to_price(p.value.get("tick_high")[0])) + int(tick_to_price(position.get("tick_high")[0])) ), - "liquidity": Balance.from_rao(p.value.get("liquidity")), + "liquidity": Balance.from_rao(position.get("liquidity")), "fees_tao": fees_tao, "fees_alpha": fees_alpha, } diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index c8e84fa191..625f3ff08d 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1476,9 +1476,11 @@ def get_liquidity_list( positions = [] for _, p in positions_response: - value = p.value - tick_low_idx = value["tick_low"][0] - tick_high_idx = value["tick_high"][0] + position = p.value + print(">>>", _, position) + + tick_low_idx = position["tick_low"][0] + tick_high_idx = position["tick_high"][0] tick_low = query( module="Swap", @@ -1533,7 +1535,7 @@ def get_liquidity_list( # Calculate fees earned by position fees_tao, fees_alpha = calculate_fees( - position=value, + position=position, global_fees_tao=fee_global_tao, global_fees_alpha=fee_global_alpha, tao_fees_below_low=tao_below, @@ -1546,16 +1548,17 @@ def get_liquidity_list( positions.append( LiquidityPosition( **{ - "id": p.value.get("id")[0], + "id": position.get("id")[0], "price_low": Balance.from_rao( - int(tick_to_price(p.value.get("tick_low")[0])) + int(tick_to_price(position.get("tick_low")[0])) ), "price_high": Balance.from_rao( - int(tick_to_price(p.value.get("tick_high")[0])) + int(tick_to_price(position.get("tick_high")[0])) ), - "liquidity": Balance.from_rao(p.value.get("liquidity")), + "liquidity": Balance.from_rao(position.get("liquidity")), "fees_tao": fees_tao, "fees_alpha": fees_alpha, + "netuid": position.get("netuid"), } ) ) From 8721560c6e7406f01e5099c612afe2c36ecb5047 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 30 Jun 2025 18:24:05 -0700 Subject: [PATCH 22/48] remove debug --- bittensor/core/subtensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 625f3ff08d..f4c9a0bc64 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1477,7 +1477,6 @@ def get_liquidity_list( positions = [] for _, p in positions_response: position = p.value - print(">>>", _, position) tick_low_idx = position["tick_low"][0] tick_high_idx = position["tick_high"][0] From 7e2e5f19e407df85fd94382e3ee806841514e810 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 30 Jun 2025 18:25:17 -0700 Subject: [PATCH 23/48] add unit test for sync `get_liquidity_list` --- tests/unit_tests/test_subtensor.py | 115 +++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 0704fc16ab..672b26dd67 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -3875,3 +3875,118 @@ def test_set_children(subtensor, fake_wallet, mocker): period=None, ) assert result == mocked_set_children_extrinsic.return_value + + +def test_get_liquidity_list_subnet_does_not_exits(subtensor, mocker): + """Test get_liquidity_list returns None when subnet doesn't exist.""" + # Preps + mocker.patch.object(subtensor, "subnet_exists", return_value=False) + + # Call + result = subtensor.get_liquidity_list(wallet=mocker.Mock(), netuid=1) + + # Asserts + subtensor.subnet_exists.assert_called_once_with(netuid=1) + assert result is None + + +def test_get_liquidity_list_subnet_is_not_active(subtensor, mocker): + """Test get_liquidity_list returns None when subnet is not active.""" + # Preps + mocker.patch.object(subtensor, "subnet_exists", return_value=True) + mocker.patch.object(subtensor, "is_subnet_active", return_value=False) + + # Call + result = subtensor.get_liquidity_list(wallet=mocker.Mock(), netuid=1) + + # Asserts + subtensor.subnet_exists.assert_called_once_with(netuid=1) + subtensor.is_subnet_active.assert_called_once_with(netuid=1) + assert result is None + + +def test_get_liquidity_list_happy_path(subtensor, fake_wallet, mocker): + """Tests `get_liquidity_list` returns the correct value.""" + # Preps + netuid = 2 + + mocker.patch.object(subtensor, "subnet_exists", return_value=True) + mocker.patch.object(subtensor, "is_subnet_active", return_value=True) + mocker.patch.object(subtensor, "determine_block_hash") + + mocker.patch.object( + subtensor_module, "price_to_tick", return_value=Balance.from_tao(1.0, netuid) + ) + mocker.patch.object( + subtensor_module, + "calculate_fees", + return_value=(Balance.from_tao(0.0), Balance.from_tao(0.0, netuid)), + ) + + mocked_substrate_query = mocker.MagicMock() + mocker.patch.object(subtensor.substrate, "query", mocked_substrate_query) + + fake_positions = [ + [ + (2,), + mocker.Mock( + value={ + "id": (2,), + "netuid": 2, + "tick_low": (206189,), + "tick_high": (208196,), + "liquidity": 1000000000000, + "fees_tao": {"bits": 0}, + "fees_alpha": {"bits": 0}, + } + ), + ], + [ + (2,), + mocker.Mock( + value={ + "id": (2,), + "netuid": 2, + "tick_low": (216189,), + "tick_high": (198196,), + "liquidity": 2000000000000, + "fees_tao": {"bits": 0}, + "fees_alpha": {"bits": 0}, + } + ), + ], + [ + (2,), + mocker.Mock( + value={ + "id": (2,), + "netuid": 2, + "tick_low": (226189,), + "tick_high": (188196,), + "liquidity": 3000000000000, + "fees_tao": {"bits": 0}, + "fees_alpha": {"bits": 0}, + } + ), + ], + ] + mocked_query_map = mocker.MagicMock(return_value=fake_positions) + mocker.patch.object(subtensor, "query_map", new=mocked_query_map) + + # Call + + result = subtensor.get_liquidity_list(wallet=fake_wallet, netuid=netuid) + + # Asserts + subtensor.determine_block_hash.assert_called_once_with(None) + assert subtensor_module.price_to_tick.call_count == 1 + assert subtensor_module.calculate_fees.call_count == len(fake_positions) + + mocked_query_map.assert_called_once_with( + module="Swap", + name="Positions", + block=None, + params=[netuid, fake_wallet.coldkeypub.ss58_address], + ) + assert len(result) == len(fake_positions) + assert all([isinstance(p, subtensor_module.LiquidityPosition) for p in result]) From 322d58f1602ea8294816a28e5089b23d4153a4f7 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 30 Jun 2025 18:58:24 -0700 Subject: [PATCH 24/48] opps fix async `get_liquidity_list` --- bittensor/core/async_subtensor.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 8133433e8d..ddf24cc7e4 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -1891,6 +1891,11 @@ async def get_liquidity_list( params=[netuid, wallet.coldkeypub.ss58_address], ), ) + # convert to floats + fee_global_tao = fixed_to_float(fee_global_tao) + fee_global_alpha = fixed_to_float(fee_global_alpha) + sqrt_price = fixed_to_float(sqrt_price) + # Fetch global fees and current price current_tick = price_to_tick(sqrt_price**2) @@ -1980,6 +1985,7 @@ async def get_liquidity_list( "liquidity": Balance.from_rao(position.get("liquidity")), "fees_tao": fees_tao, "fees_alpha": fees_alpha, + "netuid": position.get("netuid"), } ) ) From 944b3f3541286a689aa4a206ae1e267df7ff835c Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 30 Jun 2025 18:58:42 -0700 Subject: [PATCH 25/48] add async unit tests for `get_liquidity_list` --- tests/unit_tests/test_async_subtensor.py | 164 +++++++++++++++++++++++ 1 file changed, 164 insertions(+) diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index ff537e800d..81ede54139 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -3490,3 +3490,167 @@ async def test_get_next_epoch_start_block(mocker, subtensor, call_return, expect netuid=netuid, block=block, block_hash=fake_block_hash, reuse_block=False ) assert result == expected + + +@pytest.mark.asyncio +async def test_get_liquidity_list_subnet_does_not_exits(subtensor, mocker): + """Test get_liquidity_list returns None when subnet doesn't exist.""" + # Preps + mocker.patch.object(subtensor, "subnet_exists", return_value=False) + + # Call + result = await subtensor.get_liquidity_list(wallet=mocker.Mock(), netuid=1) + + # Asserts + subtensor.subnet_exists.assert_awaited_once_with(netuid=1) + assert result is None + + +@pytest.mark.asyncio +async def test_get_liquidity_list_subnet_is_not_active(subtensor, mocker): + """Test get_liquidity_list returns None when subnet is not active.""" + # Preps + mocker.patch.object(subtensor, "subnet_exists", return_value=True) + mocker.patch.object(subtensor, "is_subnet_active", return_value=False) + + # Call + result = await subtensor.get_liquidity_list(wallet=mocker.Mock(), netuid=1) + + # Asserts + subtensor.subnet_exists.assert_awaited_once_with(netuid=1) + subtensor.is_subnet_active.assert_awaited_once_with(netuid=1) + assert result is None + + +@pytest.mark.asyncio +async def test_get_liquidity_list_happy_path(subtensor, fake_wallet, mocker): + """Tests `get_liquidity_list` returns the correct value.""" + # Preps + netuid = 2 + + mocker.patch.object(subtensor, "subnet_exists", return_value=True) + mocker.patch.object(subtensor, "is_subnet_active", return_value=True) + mocker.patch.object(subtensor, "determine_block_hash") + + mocker.patch.object( + async_subtensor, "price_to_tick", return_value=Balance.from_tao(1.0, netuid) + ) + mocker.patch.object( + async_subtensor, + "calculate_fees", + return_value=(Balance.from_tao(0.0), Balance.from_tao(0.0, netuid)), + ) + + mocked_substrate_query = mocker.AsyncMock( + side_effect=[ + # for gather + {"bits": 0}, + {"bits": 0}, + {"bits": 18446744073709551616}, + # for loop + { + "liquidity_net": 1000000000000, + "liquidity_gross": 1000000000000, + "fees_out_tao": {"bits": 0}, + "fees_out_alpha": {"bits": 0}, + }, + { + "liquidity_net": -1000000000000, + "liquidity_gross": 1000000000000, + "fees_out_tao": {"bits": 0}, + "fees_out_alpha": {"bits": 0}, + }, + { + "liquidity_net": 1000000000000, + "liquidity_gross": 1000000000000, + "fees_out_tao": {"bits": 0}, + "fees_out_alpha": {"bits": 0}, + }, + { + "liquidity_net": -1000000000000, + "liquidity_gross": 1000000000000, + "fees_out_tao": {"bits": 0}, + "fees_out_alpha": {"bits": 0}, + }, + { + "liquidity_net": 1000000000000, + "liquidity_gross": 1000000000000, + "fees_out_tao": {"bits": 0}, + "fees_out_alpha": {"bits": 0}, + }, + { + "liquidity_net": -1000000000000, + "liquidity_gross": 1000000000000, + "fees_out_tao": {"bits": 0}, + "fees_out_alpha": {"bits": 0}, + }, + ] + ) + mocker.patch.object(subtensor.substrate, "query", mocked_substrate_query) + + fake_positions = [ + [ + (2,), + mocker.Mock( + value={ + "id": (2,), + "netuid": 2, + "tick_low": (206189,), + "tick_high": (208196,), + "liquidity": 1000000000000, + "fees_tao": {"bits": 0}, + "fees_alpha": {"bits": 0}, + } + ), + ], + [ + (2,), + mocker.Mock( + value={ + "id": (2,), + "netuid": 2, + "tick_low": (216189,), + "tick_high": (198196,), + "liquidity": 2000000000000, + "fees_tao": {"bits": 0}, + "fees_alpha": {"bits": 0}, + } + ), + ], + [ + (2,), + mocker.Mock( + value={ + "id": (2,), + "netuid": 2, + "tick_low": (226189,), + "tick_high": (188196,), + "liquidity": 3000000000000, + "fees_tao": {"bits": 0}, + "fees_alpha": {"bits": 0}, + } + ), + ], + ] + mocked_query_map = mocker.AsyncMock(return_value=fake_positions) + mocker.patch.object(subtensor, "query_map", new=mocked_query_map) + + # Call + + result = await subtensor.get_liquidity_list(wallet=fake_wallet, netuid=netuid) + + # Asserts + subtensor.determine_block_hash.assert_awaited_once_with( + block=None, block_hash=None, reuse_block=False + ) + assert async_subtensor.price_to_tick.call_count == 1 + assert async_subtensor.calculate_fees.call_count == len(fake_positions) + + mocked_query_map.assert_awaited_once_with( + module="Swap", + name="Positions", + block=None, + params=[netuid, fake_wallet.coldkeypub.ss58_address], + ) + assert len(result) == len(fake_positions) + assert all([isinstance(p, async_subtensor.LiquidityPosition) for p in result]) From e42e4e776122a4d7d256d63b71f67a1ec6ac18ac Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 30 Jun 2025 19:12:34 -0700 Subject: [PATCH 26/48] fix default arguments for toggle_user_liquidity --- bittensor/core/subtensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index f4c9a0bc64..f8abf6c735 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -3385,8 +3385,8 @@ def remove_liquidity( wallet: "Wallet", netuid: int, position_id: int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, period: Optional[int] = None, ) -> tuple[bool, str]: """Remove liquidity and credit balances back to wallet's hotkey stake. From e37405b1bf69ed722045db89f1a717288e43416e Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 30 Jun 2025 19:16:04 -0700 Subject: [PATCH 27/48] fix default arguments for remove_liquidity --- bittensor/core/async_subtensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index ddf24cc7e4..f0f99a7a72 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -4163,8 +4163,8 @@ async def remove_liquidity( wallet: "Wallet", netuid: int, position_id: int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, period: Optional[int] = None, ) -> tuple[bool, str]: """Remove liquidity and credit balances back to wallet's hotkey stake. From 12cc33c6553ee525e493d8abbb63670e6854ab41 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 30 Jun 2025 19:16:27 -0700 Subject: [PATCH 28/48] add unit tests for async subtensor --- tests/unit_tests/test_async_subtensor.py | 123 +++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 81ede54139..8c6e3b4a09 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -3654,3 +3654,126 @@ async def test_get_liquidity_list_happy_path(subtensor, fake_wallet, mocker): ) assert len(result) == len(fake_positions) assert all([isinstance(p, async_subtensor.LiquidityPosition) for p in result]) + + +@pytest.mark.asyncio +async def test_add_liquidity(subtensor, fake_wallet, mocker): + """Test add_liquidity extrinsic calls properly.""" + # preps + netuid = 123 + mocked_extrinsic = mocker.patch.object(async_subtensor, "add_liquidity_extrinsic") + + # Call + result = await subtensor.add_liquidity( + wallet=fake_wallet, + netuid=netuid, + liquidity=Balance.from_tao(150), + price_low=Balance.from_tao(180).rao, + price_high=Balance.from_tao(130).rao, + ) + + # Asserts + mocked_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + liquidity=Balance.from_tao(150), + price_low=Balance.from_tao(180).rao, + price_high=Balance.from_tao(130).rao, + wait_for_inclusion=True, + wait_for_finalization=False, + period=None, + ) + assert result == mocked_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_modify_liquidity(subtensor, fake_wallet, mocker): + """Test modify_liquidity extrinsic calls properly.""" + # preps + netuid = 123 + mocked_extrinsic = mocker.patch.object( + async_subtensor, "modify_liquidity_extrinsic" + ) + position_id = 2 + + # Call + result = await subtensor.modify_liquidity( + wallet=fake_wallet, + netuid=netuid, + position_id=position_id, + liquidity_delta=Balance.from_tao(150), + ) + + # Asserts + mocked_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + position_id=position_id, + liquidity_delta=Balance.from_tao(150), + wait_for_inclusion=True, + wait_for_finalization=False, + period=None, + ) + assert result == mocked_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_remove_liquidity(subtensor, fake_wallet, mocker): + """Test remove_liquidity extrinsic calls properly.""" + # preps + netuid = 123 + mocked_extrinsic = mocker.patch.object( + async_subtensor, "remove_liquidity_extrinsic" + ) + position_id = 2 + + # Call + result = await subtensor.remove_liquidity( + wallet=fake_wallet, + netuid=netuid, + position_id=position_id, + ) + + # Asserts + mocked_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + position_id=position_id, + wait_for_inclusion=True, + wait_for_finalization=False, + period=None, + ) + assert result == mocked_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_toggle_user_liquidity(subtensor, fake_wallet, mocker): + """Test toggle_user_liquidity extrinsic calls properly.""" + # preps + netuid = 123 + mocked_extrinsic = mocker.patch.object( + async_subtensor, "toggle_user_liquidity_extrinsic" + ) + enable = mocker.Mock() + + # Call + result = await subtensor.toggle_user_liquidity( + wallet=fake_wallet, + netuid=netuid, + enable=enable, + ) + + # Asserts + mocked_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + enable=enable, + wait_for_inclusion=True, + wait_for_finalization=False, + period=None, + ) + assert result == mocked_extrinsic.return_value From bb3c9f8b72cbf02a490da307d2a559f67cba83a1 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 30 Jun 2025 19:16:32 -0700 Subject: [PATCH 29/48] add unit tests for sync subtensor --- tests/unit_tests/test_subtensor.py | 119 +++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 672b26dd67..7d7dadfd41 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -3990,3 +3990,122 @@ def test_get_liquidity_list_happy_path(subtensor, fake_wallet, mocker): ) assert len(result) == len(fake_positions) assert all([isinstance(p, subtensor_module.LiquidityPosition) for p in result]) + + +def test_add_liquidity(subtensor, fake_wallet, mocker): + """Test add_liquidity extrinsic calls properly.""" + # preps + netuid = 123 + mocked_extrinsic = mocker.patch.object(subtensor_module, "add_liquidity_extrinsic") + + # Call + result = subtensor.add_liquidity( + wallet=fake_wallet, + netuid=netuid, + liquidity=Balance.from_tao(150), + price_low=Balance.from_tao(180).rao, + price_high=Balance.from_tao(130).rao, + ) + + # Asserts + mocked_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + liquidity=Balance.from_tao(150), + price_low=Balance.from_tao(180).rao, + price_high=Balance.from_tao(130).rao, + wait_for_inclusion=True, + wait_for_finalization=False, + period=None, + ) + assert result == mocked_extrinsic.return_value + + +def test_modify_liquidity(subtensor, fake_wallet, mocker): + """Test modify_liquidity extrinsic calls properly.""" + # preps + netuid = 123 + mocked_extrinsic = mocker.patch.object( + subtensor_module, "modify_liquidity_extrinsic" + ) + position_id = 2 + + # Call + result = subtensor.modify_liquidity( + wallet=fake_wallet, + netuid=netuid, + position_id=position_id, + liquidity_delta=Balance.from_tao(150), + ) + + # Asserts + mocked_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + position_id=position_id, + liquidity_delta=Balance.from_tao(150), + wait_for_inclusion=True, + wait_for_finalization=False, + period=None, + ) + assert result == mocked_extrinsic.return_value + + +def test_remove_liquidity(subtensor, fake_wallet, mocker): + """Test remove_liquidity extrinsic calls properly.""" + # preps + netuid = 123 + mocked_extrinsic = mocker.patch.object( + subtensor_module, "remove_liquidity_extrinsic" + ) + position_id = 2 + + # Call + result = subtensor.remove_liquidity( + wallet=fake_wallet, + netuid=netuid, + position_id=position_id, + ) + + # Asserts + mocked_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + position_id=position_id, + wait_for_inclusion=True, + wait_for_finalization=False, + period=None, + ) + assert result == mocked_extrinsic.return_value + + +def test_toggle_user_liquidity(subtensor, fake_wallet, mocker): + """Test toggle_user_liquidity extrinsic calls properly.""" + # preps + netuid = 123 + mocked_extrinsic = mocker.patch.object( + subtensor_module, "toggle_user_liquidity_extrinsic" + ) + enable = mocker.Mock() + + # Call + result = subtensor.toggle_user_liquidity( + wallet=fake_wallet, + netuid=netuid, + enable=enable, + ) + + # Asserts + mocked_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + enable=enable, + wait_for_inclusion=True, + wait_for_finalization=False, + period=None, + ) + assert result == mocked_extrinsic.return_value From 8c9fbaef39a0db646548ddc4cba0026bd2875984 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 1 Jul 2025 10:44:03 -0700 Subject: [PATCH 30/48] add unit tests for sync extrinsic module --- tests/unit_tests/extrinsics/test_liquidity.py | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 tests/unit_tests/extrinsics/test_liquidity.py diff --git a/tests/unit_tests/extrinsics/test_liquidity.py b/tests/unit_tests/extrinsics/test_liquidity.py new file mode 100644 index 0000000000..39f1f480fc --- /dev/null +++ b/tests/unit_tests/extrinsics/test_liquidity.py @@ -0,0 +1,169 @@ +from bittensor.core.extrinsics import liquidity + + +def test_add_liquidity_extrinsic(subtensor, fake_wallet, mocker): + """Test that the add `add_liquidity_extrinsic` executes correct calls.""" + # Preps + fake_netuid = 1 + fake_liquidity = mocker.Mock() + fake_price_low = mocker.Mock() + fake_price_high = mocker.Mock() + + mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + mocked_price_to_tick = mocker.patch.object(liquidity, "price_to_tick") + + # Call + result = liquidity.add_liquidity_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + liquidity=fake_liquidity, + price_low=fake_price_low, + price_high=fake_price_high, + ) + + # Asserts + mocked_compose_call.assert_called_once_with( + call_module="Swap", + call_function="add_liquidity", + call_params={ + "hotkey": fake_wallet.hotkey.ss58_address, + "netuid": fake_netuid, + "tick_low": mocked_price_to_tick.return_value, + "tick_high": mocked_price_to_tick.return_value, + "liquidity": fake_liquidity, + }, + ) + mocked_sign_and_send_extrinsic.assert_called_once_with( + call=mocked_compose_call.return_value, + wallet=fake_wallet, + wait_for_inclusion=True, + wait_for_finalization=False, + use_nonce=True, + period=None, + ) + assert result == mocked_sign_and_send_extrinsic.return_value + + +def test_modify_liquidity_extrinsic(subtensor, fake_wallet, mocker): + """Test that the add `modify_liquidity_extrinsic` executes correct calls.""" + # Preps + fake_netuid = 1 + fake_position_id = 2 + fake_liquidity_delta = mocker.Mock() + + mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # Call + result = liquidity.modify_liquidity_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + position_id=fake_position_id, + liquidity_delta=fake_liquidity_delta, + ) + + # Asserts + mocked_compose_call.assert_called_once_with( + call_module="Swap", + call_function="modify_position", + call_params={ + "hotkey": fake_wallet.hotkey.ss58_address, + "netuid": fake_netuid, + "position_id": fake_position_id, + "liquidity_delta": fake_liquidity_delta, + }, + ) + mocked_sign_and_send_extrinsic.assert_called_once_with( + call=mocked_compose_call.return_value, + wallet=fake_wallet, + wait_for_inclusion=True, + wait_for_finalization=False, + use_nonce=True, + period=None, + ) + assert result == mocked_sign_and_send_extrinsic.return_value + + +def test_remove_liquidity_extrinsic(subtensor, fake_wallet, mocker): + """Test that the add `remove_liquidity_extrinsic` executes correct calls.""" + # Preps + fake_netuid = 1 + fake_position_id = 2 + + mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # Call + result = liquidity.remove_liquidity_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + position_id=fake_position_id, + ) + + # Asserts + mocked_compose_call.assert_called_once_with( + call_module="Swap", + call_function="remove_liquidity", + call_params={ + "hotkey": fake_wallet.hotkey.ss58_address, + "netuid": fake_netuid, + "position_id": fake_position_id, + }, + ) + mocked_sign_and_send_extrinsic.assert_called_once_with( + call=mocked_compose_call.return_value, + wallet=fake_wallet, + wait_for_inclusion=True, + wait_for_finalization=False, + use_nonce=True, + period=None, + ) + assert result == mocked_sign_and_send_extrinsic.return_value + + +def test_toggle_user_liquidity_extrinsic(subtensor, fake_wallet, mocker): + """Test that the add `toggle_user_liquidity_extrinsic` executes correct calls.""" + # Preps + fake_netuid = 1 + fake_enable = mocker.Mock() + + mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # Call + result = liquidity.toggle_user_liquidity_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + enable=fake_enable, + ) + + # Asserts + mocked_compose_call.assert_called_once_with( + call_module="Swap", + call_function="toggle_user_liquidity", + call_params={ + "netuid": fake_netuid, + "enable": fake_enable, + }, + ) + mocked_sign_and_send_extrinsic.assert_called_once_with( + call=mocked_compose_call.return_value, + wallet=fake_wallet, + wait_for_inclusion=True, + wait_for_finalization=False, + period=None, + ) + assert result == mocked_sign_and_send_extrinsic.return_value From 55f039f71d662ed9ffa0c52ccdebd038e932672b Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 1 Jul 2025 10:44:13 -0700 Subject: [PATCH 31/48] add unit tests for async extrinsic module --- .../extrinsics/asyncex/test_liquidity.py | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 tests/unit_tests/extrinsics/asyncex/test_liquidity.py diff --git a/tests/unit_tests/extrinsics/asyncex/test_liquidity.py b/tests/unit_tests/extrinsics/asyncex/test_liquidity.py new file mode 100644 index 0000000000..1b98762218 --- /dev/null +++ b/tests/unit_tests/extrinsics/asyncex/test_liquidity.py @@ -0,0 +1,174 @@ +import pytest +from bittensor.core.extrinsics.asyncex import liquidity + + +@pytest.mark.asyncio +async def test_add_liquidity_extrinsic(subtensor, fake_wallet, mocker): + """Test that the add `add_liquidity_extrinsic` executes correct calls.""" + # Preps + fake_netuid = 1 + fake_liquidity = mocker.Mock() + fake_price_low = mocker.Mock() + fake_price_high = mocker.Mock() + + mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + mocked_price_to_tick = mocker.patch.object(liquidity, "price_to_tick") + + # Call + result = await liquidity.add_liquidity_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + liquidity=fake_liquidity, + price_low=fake_price_low, + price_high=fake_price_high, + ) + + # Asserts + mocked_compose_call.assert_awaited_once_with( + call_module="Swap", + call_function="add_liquidity", + call_params={ + "hotkey": fake_wallet.hotkey.ss58_address, + "netuid": fake_netuid, + "tick_low": mocked_price_to_tick.return_value, + "tick_high": mocked_price_to_tick.return_value, + "liquidity": fake_liquidity, + }, + ) + mocked_sign_and_send_extrinsic.assert_awaited_once_with( + call=mocked_compose_call.return_value, + wallet=fake_wallet, + wait_for_inclusion=True, + wait_for_finalization=False, + use_nonce=True, + period=None, + ) + assert result == mocked_sign_and_send_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_modify_liquidity_extrinsic(subtensor, fake_wallet, mocker): + """Test that the add `modify_liquidity_extrinsic` executes correct calls.""" + # Preps + fake_netuid = 1 + fake_position_id = 2 + fake_liquidity_delta = mocker.Mock() + + mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # Call + result = await liquidity.modify_liquidity_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + position_id=fake_position_id, + liquidity_delta=fake_liquidity_delta, + ) + + # Asserts + mocked_compose_call.assert_awaited_once_with( + call_module="Swap", + call_function="modify_position", + call_params={ + "hotkey": fake_wallet.hotkey.ss58_address, + "netuid": fake_netuid, + "position_id": fake_position_id, + "liquidity_delta": fake_liquidity_delta, + }, + ) + mocked_sign_and_send_extrinsic.assert_awaited_once_with( + call=mocked_compose_call.return_value, + wallet=fake_wallet, + wait_for_inclusion=True, + wait_for_finalization=False, + use_nonce=True, + period=None, + ) + assert result == mocked_sign_and_send_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_remove_liquidity_extrinsic(subtensor, fake_wallet, mocker): + """Test that the add `remove_liquidity_extrinsic` executes correct calls.""" + # Preps + fake_netuid = 1 + fake_position_id = 2 + + mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # Call + result = await liquidity.remove_liquidity_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + position_id=fake_position_id, + ) + + # Asserts + mocked_compose_call.assert_awaited_once_with( + call_module="Swap", + call_function="remove_liquidity", + call_params={ + "hotkey": fake_wallet.hotkey.ss58_address, + "netuid": fake_netuid, + "position_id": fake_position_id, + }, + ) + mocked_sign_and_send_extrinsic.assert_awaited_once_with( + call=mocked_compose_call.return_value, + wallet=fake_wallet, + wait_for_inclusion=True, + wait_for_finalization=False, + use_nonce=True, + period=None, + ) + assert result == mocked_sign_and_send_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_toggle_user_liquidity_extrinsic(subtensor, fake_wallet, mocker): + """Test that the add `toggle_user_liquidity_extrinsic` executes correct calls.""" + # Preps + fake_netuid = 1 + fake_enable = mocker.Mock() + + mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # Call + result = await liquidity.toggle_user_liquidity_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + enable=fake_enable, + ) + + # Asserts + mocked_compose_call.assert_awaited_once_with( + call_module="Swap", + call_function="toggle_user_liquidity", + call_params={ + "netuid": fake_netuid, + "enable": fake_enable, + }, + ) + mocked_sign_and_send_extrinsic.assert_awaited_once_with( + call=mocked_compose_call.return_value, + wallet=fake_wallet, + wait_for_inclusion=True, + wait_for_finalization=False, + period=None, + ) + assert result == mocked_sign_and_send_extrinsic.return_value From d8c566f9da886a1de3d7a0a3fb411689c762136c Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 1 Jul 2025 11:02:43 -0700 Subject: [PATCH 32/48] add unit tests for liquidity utils module --- .../unit_tests/utils/test_liquidity_utils.py | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 tests/unit_tests/utils/test_liquidity_utils.py diff --git a/tests/unit_tests/utils/test_liquidity_utils.py b/tests/unit_tests/utils/test_liquidity_utils.py new file mode 100644 index 0000000000..5a761cc6a7 --- /dev/null +++ b/tests/unit_tests/utils/test_liquidity_utils.py @@ -0,0 +1,124 @@ +import math + +import pytest + +from bittensor.utils.balance import Balance +from bittensor.utils.liquidity import ( + LiquidityPosition, + price_to_tick, + tick_to_price, + get_fees, + get_fees_in_range, + calculate_fees, +) + + +def test_liquidity_position_to_token_amounts(): + """Test conversion of liquidity position to token amounts.""" + # Preps + pos = LiquidityPosition( + id=1, + price_low=Balance.from_tao(10000), + price_high=Balance.from_tao(40000), + liquidity=Balance.from_tao(25000), + fees_tao=Balance.from_tao(0), + fees_alpha=Balance.from_tao(0), + netuid=1, + ) + current_price = Balance.from_tao(20000) + # Call + alpha, tao = pos.to_token_amounts(current_price) + # Asserts + assert isinstance(alpha, Balance) + assert isinstance(tao, Balance) + assert alpha.rao >= 0 and tao.rao >= 0 + + +def test_price_to_tick_and_back(): + """Test price to tick conversion and back.""" + # Preps + price = 1.25 + # Call + tick = price_to_tick(price) + restored_price = tick_to_price(tick) + # Asserts + assert math.isclose(restored_price, price, rel_tol=1e-3) + + +def test_price_to_tick_invalid(): + """Test price to tick conversion with invalid input.""" + with pytest.raises(ValueError): + price_to_tick(0) + + +def test_tick_to_price_invalid(): + """Test tick to price conversion with invalid input.""" + with pytest.raises(ValueError): + tick_to_price(1_000_000) + + +def test_get_fees_above_true(): + """Test fee calculation for above position.""" + # Preps + tick = { + "liquidity_net": 1000000000000, + "liquidity_gross": 1000000000000, + "fees_out_tao": {"bits": 0}, + "fees_out_alpha": {"bits": 0}, + } + # Call + result = get_fees( + current_tick=100, + tick=tick, + tick_index=90, + quote=True, + global_fees_tao=8000, + global_fees_alpha=6000, + above=True, + ) + # Asserts + assert result == 8000 + + +def test_get_fees_in_range(): + """Test fee calculation within a range.""" + # Call + value = get_fees_in_range( + quote=True, + global_fees_tao=10000, + global_fees_alpha=5000, + fees_below_low=2000, + fees_above_high=1000, + ) + # Asserts + assert value == 7000 + + +def test_calculate_fees(): + """Test calculation of fees for a position.""" + # Preps + position = { + "id": (2,), + "netuid": 2, + "tick_low": (206189,), + "tick_high": (208196,), + "liquidity": 1000000000000, + "fees_tao": {"bits": 0}, + "fees_alpha": {"bits": 0}, + } + # Call + result = calculate_fees( + position=position, + global_fees_tao=5000, + global_fees_alpha=8000, + tao_fees_below_low=1000, + tao_fees_above_high=1000, + alpha_fees_below_low=2000, + alpha_fees_above_high=1000, + netuid=1, + ) + # Asserts + assert isinstance(result[0], Balance) + assert isinstance(result[1], Balance) + assert result[0].rao > 0 + assert result[1].rao > 0 From 6973c6e8237130cee3baf44d903710f134a9396d Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 1 Jul 2025 12:07:14 -0700 Subject: [PATCH 33/48] add e2e tests for liquidity logic --- tests/e2e_tests/test_liquidity.py | 215 ++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 tests/e2e_tests/test_liquidity.py diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py new file mode 100644 index 0000000000..f474ad634a --- /dev/null +++ b/tests/e2e_tests/test_liquidity.py @@ -0,0 +1,215 @@ +import pytest + +from bittensor import Balance +from tests.e2e_tests.utils.e2e_test_utils import wait_to_start_call +from bittensor.utils.liquidity import LiquidityPosition + + +@pytest.mark.asyncio +async def test_liquidity(local_chain, subtensor, alice_wallet, bob_wallet): + """ + Tests the liquidity mechanism + + Steps: + 1. Check `get_liquidity_list` return None if SN doesn't exist. + 2. Register a subnet through Alice. + 3. Make sure `get_liquidity_list` return None without activ SN. + 4. Wait until start call availability and do this call. + 5. Add liquidity to the subnet and check `get_liquidity_list` return liquidity positions. + 6. Modify liquidity and check `get_liquidity_list` return new liquidity positions with modified liquidity value. + 7. Add second liquidity position and check `get_liquidity_list` return new liquidity positions with 0 index. + 8. Remove all liquidity positions and check `get_liquidity_list` return empty list. + """ + + alice_subnet_netuid = subtensor.get_total_subnets() # 2 + + # Make sure `get_liquidity_list` return None if SN doesn't exist + assert ( + subtensor.get_liquidity_list(wallet=alice_wallet, netuid=alice_subnet_netuid) + is None + ), "❌ `get_liquidity_list` is not None for unexisting subnet." + + # Register root as Alice + assert subtensor.register_subnet(alice_wallet), "❌ Unable to register the subnet" + + # Verify subnet 2 created successfully + assert subtensor.subnets.subnet_exists(alice_subnet_netuid), ( + f"❌ Subnet {alice_subnet_netuid} wasn't created successfully" + ) + + # Make sure `get_liquidity_list` return None without activ SN + assert ( + subtensor.subnets.get_liquidity_list( + wallet=alice_wallet, netuid=alice_subnet_netuid + ) + is None + ), "❌ `get_liquidity_list` is not None when no activ subnet." + + # Wait until start call availability and do this call + assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) + + # Make sure `get_liquidity_list` return None without activ SN + assert ( + subtensor.subnets.get_liquidity_list( + wallet=alice_wallet, netuid=alice_subnet_netuid + ) + == [] + ), "❌ `get_liquidity_list` is not empty list before fist liquidity add." + + # enable user liquidity in SN + success, message = subtensor.extrinsics.toggle_user_liquidity( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + enable=True, + ) + assert success, message + assert message == "", "❌ Cannot enable user liquidity." + + # Add liquidity + success, message = subtensor.extrinsics.add_liquidity( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + liquidity=Balance.from_tao(1000), + price_low=900_000_000, + price_high=1100_000_000, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert success, message + assert message == "", "❌ Cannot add liquidity." + + # Add liquidity + liquidity_positions = subtensor.subnets.get_liquidity_list( + wallet=alice_wallet, netuid=alice_subnet_netuid + ) + + assert len(liquidity_positions) == 1, ( + "❌ liquidity_positions has more than one element." + ) + + # Check if liquidity is correct + liquidity_position = liquidity_positions[0] + assert liquidity_position == LiquidityPosition( + id=2, + price_low=liquidity_position.price_low, + price_high=liquidity_position.price_high, + liquidity=Balance.from_tao(1000), + fees_tao=Balance.from_tao(0), + fees_alpha=Balance.from_tao(0, netuid=alice_subnet_netuid), + netuid=alice_subnet_netuid, + ), "❌ `get_liquidity_list` still empty list after liquidity add." + + # Modify liquidity position with positive value + success, message = subtensor.extrinsics.modify_liquidity( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + position_id=liquidity_position.id, + liquidity_delta=Balance.from_tao(500), + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert success, message + assert message == "", "❌ cannot modify liquidity position." + + liquidity_positions = subtensor.subnets.get_liquidity_list( + wallet=alice_wallet, netuid=alice_subnet_netuid + ) + + assert len(liquidity_positions) == 1, ( + "❌ liquidity_positions has more than one element." + ) + liquidity_position = liquidity_positions[0] + + assert liquidity_position == LiquidityPosition( + id=2, + price_low=liquidity_position.price_low, + price_high=liquidity_position.price_high, + liquidity=Balance.from_tao(1500), + fees_tao=Balance.from_tao(0), + fees_alpha=Balance.from_tao(0, netuid=alice_subnet_netuid), + netuid=alice_subnet_netuid, + ) + + # Modify liquidity position with negative value + success, message = subtensor.extrinsics.modify_liquidity( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + position_id=liquidity_position.id, + liquidity_delta=-Balance.from_tao(750), + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert success, message + assert message == "", "❌ cannot modify liquidity position." + + liquidity_positions = subtensor.subnets.get_liquidity_list( + wallet=alice_wallet, netuid=alice_subnet_netuid + ) + + assert len(liquidity_positions) == 1, ( + "❌ liquidity_positions has more than one element." + ) + liquidity_position = liquidity_positions[0] + + assert liquidity_position == LiquidityPosition( + id=2, + price_low=liquidity_position.price_low, + price_high=liquidity_position.price_high, + liquidity=Balance.from_tao(750), + fees_tao=Balance.from_tao(0), + fees_alpha=Balance.from_tao(0, netuid=alice_subnet_netuid), + netuid=alice_subnet_netuid, + ) + + # Add second liquidity position + success, message = subtensor.extrinsics.add_liquidity( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + liquidity=Balance.from_tao(150), + price_low=800_000_000, + price_high=1200_000_000, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert success, message + assert message == "", "❌ Cannot add liquidity." + + liquidity_positions = subtensor.subnets.get_liquidity_list( + wallet=alice_wallet, netuid=alice_subnet_netuid + ) + + assert len(liquidity_positions) == 2, ( + f"❌ liquidity_positions should have 2 elements, but has only {len(liquidity_positions)} element." + ) + + # All new liquidity inserts on the 0 index + liquidity_position = liquidity_positions[0] + assert liquidity_position == LiquidityPosition( + id=3, + price_low=liquidity_position.price_low, + price_high=liquidity_position.price_high, + liquidity=Balance.from_tao(150), + fees_tao=Balance.from_tao(0), + fees_alpha=Balance.from_tao(0, netuid=alice_subnet_netuid), + netuid=alice_subnet_netuid, + ) + + # Remove all liquidity positions + for p in liquidity_positions: + success, message = subtensor.extrinsics.remove_liquidity( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + position_id=p.id, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert success, message + assert message == "", "❌ Cannot remove liquidity." + + # Make sure all liquidity positions removed + assert ( + subtensor.subnets.get_liquidity_list( + wallet=alice_wallet, netuid=alice_subnet_netuid + ) + == [] + ), "❌ Not all liquidity positions removed." From 96361364423ef881c8529d4b9a34494da49f000a Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 1 Jul 2025 12:37:51 -0700 Subject: [PATCH 34/48] improve e2e test for non-fast-block --- tests/e2e_tests/test_liquidity.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py index f474ad634a..6e2a6243ea 100644 --- a/tests/e2e_tests/test_liquidity.py +++ b/tests/e2e_tests/test_liquidity.py @@ -65,6 +65,15 @@ async def test_liquidity(local_chain, subtensor, alice_wallet, bob_wallet): assert success, message assert message == "", "❌ Cannot enable user liquidity." + # Add stake to herself to have Alpha (affect non-fast-blocks chain) + assert subtensor.extrinsics.add_stake( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + amount=Balance.from_tao(100), + wait_for_inclusion=True, + wait_for_finalization=True, + ), "❌ Cannot add stake." + # Add liquidity success, message = subtensor.extrinsics.add_liquidity( wallet=alice_wallet, From 8d9b245091df7f1feaa77d66087954945ca65132 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 1 Jul 2025 19:58:10 -0700 Subject: [PATCH 35/48] balance message --- bittensor/utils/balance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/utils/balance.py b/bittensor/utils/balance.py index d6c78d7d95..751bf0721f 100644 --- a/bittensor/utils/balance.py +++ b/bittensor/utils/balance.py @@ -68,7 +68,7 @@ def __init__(self, balance: Union[int, float]): # Assume tao value for the float self.rao = int(balance * pow(10, 9)) else: - raise TypeError("balance must be an int (rao) or a float (tao)") + raise TypeError(f"Balance must be an int (rao) or a float (tao). You passed: `{type(balance)}`.") @property def tao(self): From 3feab5cb9476dcf57f8c36d26f6dc5f0bbda1a89 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 1 Jul 2025 21:05:37 -0700 Subject: [PATCH 36/48] fix `bittensor.utils.liquidity.calculate_fees` --- bittensor/utils/liquidity.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/bittensor/utils/liquidity.py b/bittensor/utils/liquidity.py index 6a621990b4..55e206225c 100644 --- a/bittensor/utils/liquidity.py +++ b/bittensor/utils/liquidity.py @@ -64,16 +64,14 @@ def to_token_amounts( 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}") + raise ValueError(f"Price must be positive, got `{price}`.") - log_base = math.log1p(PRICE_STEP - 1) # safer for small deltas - tick = round(math.log(price) / log_base) + tick = int(math.log(price) / math.log(PRICE_STEP)) - if tick < MIN_TICK or tick > MAX_TICK: + 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 @@ -133,8 +131,7 @@ def calculate_fees( alpha_fees_below_low: float, alpha_fees_above_high: float, netuid: int, -) -> list[Balance]: - """Calculate fees from position and fees values.""" +) -> tuple[Balance, Balance]: fee_tao_agg = get_fees_in_range( quote=True, global_fees_tao=global_fees_tao, @@ -142,6 +139,7 @@ def calculate_fees( 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, @@ -157,4 +155,4 @@ def calculate_fees( fee_tao = liquidity_frac * fee_tao fee_alpha = liquidity_frac * fee_alpha - return [Balance.from_rao(fee_tao), Balance.from_rao(fee_alpha, netuid)] + return Balance.from_rao(int(fee_tao)), Balance.from_rao(int(fee_alpha), netuid) From bf4a8dd7613d0c780998023d4a459525623ebcdc Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 1 Jul 2025 21:06:09 -0700 Subject: [PATCH 37/48] improve Balance error message --- bittensor/utils/balance.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bittensor/utils/balance.py b/bittensor/utils/balance.py index 751bf0721f..7e5fb050fd 100644 --- a/bittensor/utils/balance.py +++ b/bittensor/utils/balance.py @@ -68,7 +68,9 @@ def __init__(self, balance: Union[int, float]): # Assume tao value for the float self.rao = int(balance * pow(10, 9)) else: - raise TypeError(f"Balance must be an int (rao) or a float (tao). You passed: `{type(balance)}`.") + raise TypeError( + f"Balance must be an int (rao) or a float (tao). You passed: `{type(balance)}`." + ) @property def tao(self): From 5bc38b830233a9819f6e0a1faab09e583eb7e0fb Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 1 Jul 2025 21:08:42 -0700 Subject: [PATCH 38/48] fix sync extrinsic --- bittensor/core/extrinsics/liquidity.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bittensor/core/extrinsics/liquidity.py b/bittensor/core/extrinsics/liquidity.py index 1cd832dc04..9e22da4139 100644 --- a/bittensor/core/extrinsics/liquidity.py +++ b/bittensor/core/extrinsics/liquidity.py @@ -15,8 +15,8 @@ def add_liquidity_extrinsic( wallet: "Wallet", netuid: int, liquidity: Balance, - price_low: float, - price_high: float, + price_low: Balance, + price_high: Balance, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, period: Optional[int] = None, @@ -49,8 +49,8 @@ def add_liquidity_extrinsic( logging.error(unlock.message) return False, unlock.message - tick_low = price_to_tick(price_low) - tick_high = price_to_tick(price_high) + tick_low = price_to_tick(price_low.tao) + tick_high = price_to_tick(price_high.tao) call = subtensor.substrate.compose_call( call_module="Swap", @@ -60,7 +60,7 @@ def add_liquidity_extrinsic( "netuid": netuid, "tick_low": tick_low, "tick_high": tick_high, - "liquidity": liquidity, + "liquidity": liquidity.rao, }, ) @@ -117,7 +117,7 @@ def modify_liquidity_extrinsic( "hotkey": wallet.hotkey.ss58_address, "netuid": netuid, "position_id": position_id, - "liquidity_delta": liquidity_delta, + "liquidity_delta": liquidity_delta.rao, }, ) From fa1702f6e6b0f4fbb9e7143d50dfb7428eb15e03 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 1 Jul 2025 21:08:47 -0700 Subject: [PATCH 39/48] fix async extrinsic --- bittensor/core/extrinsics/asyncex/liquidity.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bittensor/core/extrinsics/asyncex/liquidity.py b/bittensor/core/extrinsics/asyncex/liquidity.py index 67abe674e3..cbe43575ba 100644 --- a/bittensor/core/extrinsics/asyncex/liquidity.py +++ b/bittensor/core/extrinsics/asyncex/liquidity.py @@ -15,8 +15,8 @@ async def add_liquidity_extrinsic( wallet: "Wallet", netuid: int, liquidity: Balance, - price_low: float, - price_high: float, + price_low: Balance, + price_high: Balance, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, period: Optional[int] = None, @@ -49,8 +49,8 @@ async def add_liquidity_extrinsic( logging.error(unlock.message) return False, unlock.message - tick_low = price_to_tick(price_low) - tick_high = price_to_tick(price_high) + 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", @@ -60,7 +60,7 @@ async def add_liquidity_extrinsic( "netuid": netuid, "tick_low": tick_low, "tick_high": tick_high, - "liquidity": liquidity, + "liquidity": liquidity.rao, }, ) @@ -117,7 +117,7 @@ async def modify_liquidity_extrinsic( "hotkey": wallet.hotkey.ss58_address, "netuid": netuid, "position_id": position_id, - "liquidity_delta": liquidity_delta, + "liquidity_delta": liquidity_delta.rao, }, ) From b765276f698350a3f3f1e7b109bfd6b4bccd6e34 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 1 Jul 2025 21:16:32 -0700 Subject: [PATCH 40/48] fix subtensors --- bittensor/core/async_subtensor.py | 16 ++++----- bittensor/core/subtensor.py | 55 +++++++++++++++---------------- 2 files changed, 34 insertions(+), 37 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 73873640d7..3474935919 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -2011,11 +2011,11 @@ async def get_liquidity_list( LiquidityPosition( **{ "id": position.get("id")[0], - "price_low": Balance.from_rao( - int(tick_to_price(position.get("tick_low")[0])) + "price_low": Balance.from_tao( + tick_to_price(position.get("tick_low")[0]) ), - "price_high": Balance.from_rao( - int(tick_to_price(position.get("tick_high")[0])) + "price_high": Balance.from_tao( + tick_to_price(position.get("tick_high")[0]) ), "liquidity": Balance.from_rao(position.get("liquidity")), "fees_tao": fees_tao, @@ -3783,8 +3783,8 @@ async def add_liquidity( wallet: "Wallet", netuid: int, liquidity: Balance, - price_low: float, - price_high: float, + price_low: Balance, + price_high: Balance, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, period: Optional[int] = None, @@ -3796,8 +3796,8 @@ async def add_liquidity( 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. 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. + price_low: The lower bound of the price tick range. In TAO. + price_high: The upper bound of the price tick range. In TAO. 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. period: The number of blocks during which the transaction will remain valid after it's submitted. If diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index db70a27771..a98b703beb 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1479,30 +1479,27 @@ def get_liquidity_list( block_hash = self.determine_block_hash(block) # Fetch global fees and current price - fee_global_tao = fixed_to_float( - query( - module="Swap", - storage_function="FeeGlobalTao", - params=[netuid], - block_hash=block_hash, - ) + fee_global_tao_query = query( + module="Swap", + storage_function="FeeGlobalTao", + params=[netuid], + block_hash=block_hash, ) - fee_global_alpha = fixed_to_float( - query( - module="Swap", - storage_function="FeeGlobalAlpha", - params=[netuid], - block_hash=block_hash, - ) + fee_global_alpha_query = query( + module="Swap", + storage_function="FeeGlobalAlpha", + params=[netuid], + block_hash=block_hash, ) - sqrt_price = fixed_to_float( - query( - module="Swap", - storage_function="AlphaSqrtPrice", - params=[netuid], - block_hash=block_hash, - ) + sqrt_price_query = query( + module="Swap", + storage_function="AlphaSqrtPrice", + params=[netuid], + block_hash=block_hash, ) + fee_global_tao = fixed_to_float(fee_global_tao_query) + fee_global_alpha = fixed_to_float(fee_global_alpha_query) + sqrt_price = fixed_to_float(sqrt_price_query) current_tick = price_to_tick(sqrt_price**2) # Fetch positions @@ -1587,11 +1584,11 @@ def get_liquidity_list( LiquidityPosition( **{ "id": position.get("id")[0], - "price_low": Balance.from_rao( - int(tick_to_price(position.get("tick_low")[0])) + "price_low": Balance.from_tao( + tick_to_price(position.get("tick_low")[0]) ), - "price_high": Balance.from_rao( - int(tick_to_price(position.get("tick_high")[0])) + "price_high": Balance.from_tao( + tick_to_price(position.get("tick_high")[0]) ), "liquidity": Balance.from_rao(position.get("liquidity")), "fees_tao": fees_tao, @@ -3003,8 +3000,8 @@ def add_liquidity( wallet: "Wallet", netuid: int, liquidity: Balance, - price_low: float, - price_high: float, + price_low: Balance, + price_high: Balance, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, period: Optional[int] = None, @@ -3016,8 +3013,8 @@ def add_liquidity( 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. 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. + price_low: The lower bound of the price tick range. In TAO. + price_high: The upper bound of the price tick range. In TAO. 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. period: The number of blocks during which the transaction will remain valid after it's submitted. If From 587050a3333414ab002ae402203261bc84d6d117 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 1 Jul 2025 21:16:43 -0700 Subject: [PATCH 41/48] fix unit tests --- tests/unit_tests/extrinsics/asyncex/test_liquidity.py | 4 ++-- tests/unit_tests/extrinsics/test_liquidity.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit_tests/extrinsics/asyncex/test_liquidity.py b/tests/unit_tests/extrinsics/asyncex/test_liquidity.py index 1b98762218..ae780e6c4b 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_liquidity.py +++ b/tests/unit_tests/extrinsics/asyncex/test_liquidity.py @@ -36,7 +36,7 @@ async def test_add_liquidity_extrinsic(subtensor, fake_wallet, mocker): "netuid": fake_netuid, "tick_low": mocked_price_to_tick.return_value, "tick_high": mocked_price_to_tick.return_value, - "liquidity": fake_liquidity, + "liquidity": fake_liquidity.rao, }, ) mocked_sign_and_send_extrinsic.assert_awaited_once_with( @@ -80,7 +80,7 @@ async def test_modify_liquidity_extrinsic(subtensor, fake_wallet, mocker): "hotkey": fake_wallet.hotkey.ss58_address, "netuid": fake_netuid, "position_id": fake_position_id, - "liquidity_delta": fake_liquidity_delta, + "liquidity_delta": fake_liquidity_delta.rao, }, ) mocked_sign_and_send_extrinsic.assert_awaited_once_with( diff --git a/tests/unit_tests/extrinsics/test_liquidity.py b/tests/unit_tests/extrinsics/test_liquidity.py index 39f1f480fc..7d3942909e 100644 --- a/tests/unit_tests/extrinsics/test_liquidity.py +++ b/tests/unit_tests/extrinsics/test_liquidity.py @@ -34,7 +34,7 @@ def test_add_liquidity_extrinsic(subtensor, fake_wallet, mocker): "netuid": fake_netuid, "tick_low": mocked_price_to_tick.return_value, "tick_high": mocked_price_to_tick.return_value, - "liquidity": fake_liquidity, + "liquidity": fake_liquidity.rao, }, ) mocked_sign_and_send_extrinsic.assert_called_once_with( @@ -77,7 +77,7 @@ def test_modify_liquidity_extrinsic(subtensor, fake_wallet, mocker): "hotkey": fake_wallet.hotkey.ss58_address, "netuid": fake_netuid, "position_id": fake_position_id, - "liquidity_delta": fake_liquidity_delta, + "liquidity_delta": fake_liquidity_delta.rao, }, ) mocked_sign_and_send_extrinsic.assert_called_once_with( From 253b0bd2866e225705479964affddcf8922ef2f4 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 1 Jul 2025 21:16:49 -0700 Subject: [PATCH 42/48] fix e2e test --- tests/e2e_tests/test_liquidity.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py index 6e2a6243ea..cd063cb07b 100644 --- a/tests/e2e_tests/test_liquidity.py +++ b/tests/e2e_tests/test_liquidity.py @@ -79,8 +79,8 @@ async def test_liquidity(local_chain, subtensor, alice_wallet, bob_wallet): wallet=alice_wallet, netuid=alice_subnet_netuid, liquidity=Balance.from_tao(1000), - price_low=900_000_000, - price_high=1100_000_000, + price_low=Balance.from_tao(0.9), + price_high=Balance.from_tao(1.1), wait_for_inclusion=True, wait_for_finalization=True, ) @@ -175,8 +175,8 @@ async def test_liquidity(local_chain, subtensor, alice_wallet, bob_wallet): wallet=alice_wallet, netuid=alice_subnet_netuid, liquidity=Balance.from_tao(150), - price_low=800_000_000, - price_high=1200_000_000, + price_low=Balance.from_tao(0.8), + price_high=Balance.from_tao(1.2), wait_for_inclusion=True, wait_for_finalization=True, ) From ee90a3e07fdc5923ff084384d28f1a5523ce1af2 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 1 Jul 2025 22:07:28 -0700 Subject: [PATCH 43/48] make e2e test more difficult (add fees checks after add_stake and remove/unstake) --- tests/e2e_tests/test_liquidity.py | 81 ++++++++++++++++++++++--------- 1 file changed, 58 insertions(+), 23 deletions(-) diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py index cd063cb07b..d134089d12 100644 --- a/tests/e2e_tests/test_liquidity.py +++ b/tests/e2e_tests/test_liquidity.py @@ -18,7 +18,9 @@ async def test_liquidity(local_chain, subtensor, alice_wallet, bob_wallet): 5. Add liquidity to the subnet and check `get_liquidity_list` return liquidity positions. 6. Modify liquidity and check `get_liquidity_list` return new liquidity positions with modified liquidity value. 7. Add second liquidity position and check `get_liquidity_list` return new liquidity positions with 0 index. - 8. Remove all liquidity positions and check `get_liquidity_list` return empty list. + 8. Add stake from Bob to Alice and check `get_liquidity_list` return new liquidity positions with fees_tao. + 9. Remove all stake from Alice and check `get_liquidity_list` return new liquidity positions with 0 fees_tao. + 10. Remove all liquidity positions and check `get_liquidity_list` return empty list. """ alice_subnet_netuid = subtensor.get_total_subnets() # 2 @@ -65,29 +67,20 @@ async def test_liquidity(local_chain, subtensor, alice_wallet, bob_wallet): assert success, message assert message == "", "❌ Cannot enable user liquidity." - # Add stake to herself to have Alpha (affect non-fast-blocks chain) - assert subtensor.extrinsics.add_stake( - wallet=alice_wallet, - netuid=alice_subnet_netuid, - amount=Balance.from_tao(100), - wait_for_inclusion=True, - wait_for_finalization=True, - ), "❌ Cannot add stake." - # Add liquidity success, message = subtensor.extrinsics.add_liquidity( wallet=alice_wallet, netuid=alice_subnet_netuid, - liquidity=Balance.from_tao(1000), - price_low=Balance.from_tao(0.9), - price_high=Balance.from_tao(1.1), + liquidity=Balance.from_tao(1), + price_low=Balance.from_tao(1.7), + price_high=Balance.from_tao(1.8), wait_for_inclusion=True, wait_for_finalization=True, ) assert success, message assert message == "", "❌ Cannot add liquidity." - # Add liquidity + # Get liquidity liquidity_positions = subtensor.subnets.get_liquidity_list( wallet=alice_wallet, netuid=alice_subnet_netuid ) @@ -102,7 +95,7 @@ async def test_liquidity(local_chain, subtensor, alice_wallet, bob_wallet): id=2, price_low=liquidity_position.price_low, price_high=liquidity_position.price_high, - liquidity=Balance.from_tao(1000), + liquidity=Balance.from_tao(1), fees_tao=Balance.from_tao(0), fees_alpha=Balance.from_tao(0, netuid=alice_subnet_netuid), netuid=alice_subnet_netuid, @@ -113,7 +106,7 @@ async def test_liquidity(local_chain, subtensor, alice_wallet, bob_wallet): wallet=alice_wallet, netuid=alice_subnet_netuid, position_id=liquidity_position.id, - liquidity_delta=Balance.from_tao(500), + liquidity_delta=Balance.from_tao(20), wait_for_inclusion=True, wait_for_finalization=True, ) @@ -133,7 +126,7 @@ async def test_liquidity(local_chain, subtensor, alice_wallet, bob_wallet): id=2, price_low=liquidity_position.price_low, price_high=liquidity_position.price_high, - liquidity=Balance.from_tao(1500), + liquidity=Balance.from_tao(21), fees_tao=Balance.from_tao(0), fees_alpha=Balance.from_tao(0, netuid=alice_subnet_netuid), netuid=alice_subnet_netuid, @@ -144,7 +137,7 @@ async def test_liquidity(local_chain, subtensor, alice_wallet, bob_wallet): wallet=alice_wallet, netuid=alice_subnet_netuid, position_id=liquidity_position.id, - liquidity_delta=-Balance.from_tao(750), + liquidity_delta=-Balance.from_tao(11), wait_for_inclusion=True, wait_for_finalization=True, ) @@ -164,12 +157,22 @@ async def test_liquidity(local_chain, subtensor, alice_wallet, bob_wallet): id=2, price_low=liquidity_position.price_low, price_high=liquidity_position.price_high, - liquidity=Balance.from_tao(750), + liquidity=Balance.from_tao(10), fees_tao=Balance.from_tao(0), fees_alpha=Balance.from_tao(0, netuid=alice_subnet_netuid), netuid=alice_subnet_netuid, ) + # Add stake from Bob to Alice + assert subtensor.extrinsics.add_stake( + wallet=bob_wallet, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + amount=Balance.from_tao(1000), + wait_for_inclusion=True, + wait_for_finalization=True, + ), "❌ Cannot add stake from Bob to Alice." + # Add second liquidity position success, message = subtensor.extrinsics.add_liquidity( wallet=alice_wallet, @@ -192,17 +195,49 @@ async def test_liquidity(local_chain, subtensor, alice_wallet, bob_wallet): ) # All new liquidity inserts on the 0 index - liquidity_position = liquidity_positions[0] - assert liquidity_position == LiquidityPosition( + liquidity_position_second = liquidity_positions[0] + assert liquidity_position_second == LiquidityPosition( id=3, - price_low=liquidity_position.price_low, - price_high=liquidity_position.price_high, + price_low=liquidity_position_second.price_low, + price_high=liquidity_position_second.price_high, liquidity=Balance.from_tao(150), fees_tao=Balance.from_tao(0), fees_alpha=Balance.from_tao(0, netuid=alice_subnet_netuid), netuid=alice_subnet_netuid, ) + liquidity_position_first = liquidity_positions[1] + assert liquidity_position_first == LiquidityPosition( + id=2, + price_low=liquidity_position_first.price_low, + price_high=liquidity_position_first.price_high, + liquidity=Balance.from_tao(10), + fees_tao=liquidity_position_first.fees_tao, + fees_alpha=Balance.from_tao(0, netuid=alice_subnet_netuid), + netuid=alice_subnet_netuid, + ) + # After adding stake alice liquidity position has a fees_tao bc of high price + assert liquidity_position_first.fees_tao > Balance.from_tao(0) + + # Bob remove all stake from alice + assert subtensor.extrinsics.unstake_all( + wallet=bob_wallet, + hotkey=alice_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + rate_tolerance=0.9, # keep high rate tolerance to avoid flaky behavior + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + # Check that fees_alpha comes too after all unstake + liquidity_position_first = subtensor.subnets.get_liquidity_list( + wallet=alice_wallet, netuid=alice_subnet_netuid + )[1] + assert liquidity_position_first.fees_tao > Balance.from_tao(0) + assert liquidity_position_first.fees_alpha > Balance.from_tao( + 0, alice_subnet_netuid + ) + # Remove all liquidity positions for p in liquidity_positions: success, message = subtensor.extrinsics.remove_liquidity( From 9089c81c905a427fb7767c326a010f5ccf813d4b Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 1 Jul 2025 23:05:53 -0700 Subject: [PATCH 44/48] add `alpha for non-fast-blocks` --- tests/e2e_tests/test_liquidity.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py index d134089d12..fd9ad09d42 100644 --- a/tests/e2e_tests/test_liquidity.py +++ b/tests/e2e_tests/test_liquidity.py @@ -67,6 +67,17 @@ async def test_liquidity(local_chain, subtensor, alice_wallet, bob_wallet): assert success, message assert message == "", "❌ Cannot enable user liquidity." + # In non fast-blocks node Alice doesn't have stake + if not subtensor.chain.is_fast_blocks(): + assert subtensor.extrinsics.add_stake( + wallet=alice_wallet, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + amount=Balance.from_tao(1000), + wait_for_inclusion=True, + wait_for_finalization=True, + ), "❌ Cannot cannot add stake to Alice from Alice." + # Add liquidity success, message = subtensor.extrinsics.add_liquidity( wallet=alice_wallet, From 834972575f3ccbbc54e82971ce757280768f9e5d Mon Sep 17 00:00:00 2001 From: Roman <167799377+basfroman@users.noreply.github.com> Date: Wed, 2 Jul 2025 09:27:47 -0700 Subject: [PATCH 45/48] Update bittensor/core/async_subtensor.py Co-authored-by: BD Himes <37844818+thewhaleking@users.noreply.github.com> --- bittensor/core/async_subtensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 3474935919..1afafcdfc8 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -1936,7 +1936,7 @@ async def get_liquidity_list( # Fetch positions positions = [] - for _, p in positions_response: + async for _, p in positions_response: position = p.value tick_low_idx = position.get("tick_low")[0] From 7864c10984c2e1d6044eac46be2bd39d312c3b52 Mon Sep 17 00:00:00 2001 From: Roman <167799377+basfroman@users.noreply.github.com> Date: Wed, 2 Jul 2025 09:35:53 -0700 Subject: [PATCH 46/48] Update bittensor/utils/balance.py I have an entry for SDK v.10 to make error messages more informative. I don't want the user to have to double-check themselves to figure out what the error is. Co-authored-by: BD Himes <37844818+thewhaleking@users.noreply.github.com> --- bittensor/utils/balance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/utils/balance.py b/bittensor/utils/balance.py index 7e5fb050fd..b65fdaec82 100644 --- a/bittensor/utils/balance.py +++ b/bittensor/utils/balance.py @@ -69,7 +69,7 @@ def __init__(self, balance: Union[int, float]): self.rao = int(balance * pow(10, 9)) else: raise TypeError( - f"Balance must be an int (rao) or a float (tao). You passed: `{type(balance)}`." + f"Balance must be an int (rao) or a float (tao), not `{type(balance)}`." ) @property From 3c0957805385622ae1e10d4a527475d747931f25 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 2 Jul 2025 09:37:53 -0700 Subject: [PATCH 47/48] fix docstrings --- bittensor/core/async_subtensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 1afafcdfc8..d198e72c4d 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -4018,6 +4018,8 @@ async def modify_liquidity( import bittensor as bt subtensor = bt.AsyncSubtensor(network="local") + await subtensor.initialize() + my_wallet = bt.Wallet() # if `liquidity_delta` is negative From 0c747dfd45fce0dd15433821aa69c2c770ce8c4b Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 2 Jul 2025 09:45:23 -0700 Subject: [PATCH 48/48] fix test --- tests/unit_tests/test_async_subtensor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index b34edf9524..7e60be2ae0 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -3661,7 +3661,11 @@ async def test_get_liquidity_list_happy_path(subtensor, fake_wallet, mocker): ), ], ] - mocked_query_map = mocker.AsyncMock(return_value=fake_positions) + + fake_result = mocker.AsyncMock(autospec=list) + fake_result.__aiter__.return_value = iter(fake_positions) + + mocked_query_map = mocker.AsyncMock(return_value=fake_result) mocker.patch.object(subtensor, "query_map", new=mocked_query_map) # Call