diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 0dd68d6205..e349616236 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -76,6 +76,7 @@ ) from bittensor.core.extrinsics.asyncex.transfer import transfer_extrinsic from bittensor.core.extrinsics.asyncex.unstaking import ( + unstake_all_extrinsic, unstake_extrinsic, unstake_multiple_extrinsic, ) @@ -4636,6 +4637,90 @@ async def unstake( unstake_all=unstake_all, ) + async def unstake_all( + self, + wallet: "Wallet", + hotkey: str, + netuid: int, + rate_tolerance: Optional[float] = 0.005, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + period: Optional[int] = None, + ) -> tuple[bool, str]: + """Unstakes all TAO/Alpha associated with a hotkey from the specified subnets on the Bittensor network. + + Arguments: + wallet: The wallet of the stake owner. + hotkey: The SS58 address of the hotkey to unstake from. + netuid: The unique identifier of the subnet. + rate_tolerance: The maximum allowed price change ratio when unstaking. For example, 0.005 = 0.5% maximum + price decrease. If not passed (None), then unstaking goes without price limit. Default is 0.005. + wait_for_inclusion: Waits for the transaction to be included in a block. Default is `True`. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. Default is `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. Default is `None`. + + Returns: + tuple[bool, str]: + A tuple containing: + - `True` and a success message if the unstake operation succeeded; + - `False` and an error message otherwise. + + Example: + # If you would like to unstake all stakes in all subnets safely, use default `rate_tolerance` or pass your + value: + import bittensor as bt + + subtensor = bt.AsyncSubtensor() + wallet = bt.Wallet("my_wallet") + netuid = 14 + hotkey = "5%SOME_HOTKEY_WHERE_IS_YOUR_STAKE_NOW%" + + wallet_stakes = await subtensor.get_stake_info_for_coldkey(coldkey_ss58=wallet.coldkey.ss58_address) + + for stake in wallet_stakes: + result = await subtensor.unstake_all( + wallet=wallet, + hotkey_ss58=stake.hotkey_ss58, + netuid=stake.netuid, + ) + print(result) + + # If you would like to unstake all stakes in all subnets unsafely, use `rate_tolerance=None`: + import bittensor as bt + + subtensor = bt.AsyncSubtensor() + wallet = bt.Wallet("my_wallet") + netuid = 14 + hotkey = "5%SOME_HOTKEY_WHERE_IS_YOUR_STAKE_NOW%" + + wallet_stakes = await subtensor.get_stake_info_for_coldkey(coldkey_ss58=wallet.coldkey.ss58_address) + + for stake in wallet_stakes: + result = await subtensor.unstake_all( + wallet=wallet, + hotkey_ss58=stake.hotkey_ss58, + netuid=stake.netuid, + rate_tolerance=None, + ) + print(result) + """ + if netuid != 0: + logging.debug( + f"Unstaking without Alpha price control from subnet [blue]#{netuid}[/blue]." + ) + return await unstake_all_extrinsic( + subtensor=self, + wallet=wallet, + hotkey=hotkey, + netuid=netuid, + rate_tolerance=rate_tolerance, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + ) + async def unstake_multiple( self, wallet: "Wallet", diff --git a/bittensor/core/extrinsics/asyncex/unstaking.py b/bittensor/core/extrinsics/asyncex/unstaking.py index d06300c138..a6f00633c0 100644 --- a/bittensor/core/extrinsics/asyncex/unstaking.py +++ b/bittensor/core/extrinsics/asyncex/unstaking.py @@ -30,27 +30,28 @@ async def unstake_extrinsic( """Removes stake into the wallet coldkey from the specified hotkey ``uid``. Args: - subtensor (bittensor.core.async_subtensor.AsyncSubtensor): AsyncSubtensor instance. - wallet (bittensor_wallet.Wallet): Bittensor wallet object. - hotkey_ss58 (Optional[str]): The ``ss58`` address of the hotkey to unstake from. By default, the wallet hotkey - is used. - netuid (Optional[int]): The subnet uid to unstake from. - amount (Union[Balance, float]): Amount to stake as Bittensor balance, or ``float`` interpreted as Tao. - wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``True``, or - returns ``False`` if the extrinsic fails to enter the block within the timeout. - wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning - ``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout. + subtensor: AsyncSubtensor instance. + wallet: Bittensor wallet object. + hotkey_ss58: The ``ss58`` address of the hotkey to unstake from. By default, the wallet hotkey is used. + netuid: The subnet uid to unstake from. + amount: Amount to stake as Bittensor balance, or ``float`` interpreted as Tao. + wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning ``True``, or returns + ``False`` if the extrinsic fails to enter the block within the timeout. + wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning ``True``, + or returns ``False`` if the extrinsic fails to be finalized within the timeout. safe_staking: If true, enables price safety checks allow_partial_stake: If true, allows partial unstaking if price tolerance exceeded rate_tolerance: Maximum allowed price decrease percentage (0.005 = 0.5%) - period (Optional[int]): 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. + 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. unstake_all: If true, unstakes all tokens. Default is ``False``. Returns: - success (bool): Flag is ``True`` if extrinsic was finalized or included in the block. If we did not wait for - finalization / inclusion, the response is ``True``. + tuple[bool, str]: + A tuple containing: + - `True` and a success message if the unstake operation succeeded; + - `False` and an error message otherwise. """ if amount and unstake_all: raise ValueError("Cannot specify both `amount` and `unstake_all`.") @@ -200,6 +201,71 @@ async def unstake_extrinsic( return False +async def unstake_all_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + hotkey: str, + netuid: int, + rate_tolerance: Optional[float] = 0.005, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + period: Optional[int] = None, +) -> tuple[bool, str]: + """Unstakes all TAO/Alpha associated with a hotkey from the specified subnets on the Bittensor network. + + Arguments: + subtensor: Subtensor instance. + wallet: The wallet of the stake owner. + hotkey: The SS58 address of the hotkey to unstake from. + netuid: The unique identifier of the subnet. + rate_tolerance: The maximum allowed price change ratio when unstaking. For example, 0.005 = 0.5% maximum + price decrease. If not passed (None), then unstaking goes without price limit. Default is `0.005`. + wait_for_inclusion: Waits for the transaction to be included in a block. Default is `True`. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. Default is `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. Default is `None`. + + Returns: + tuple[bool, str]: + A tuple containing: + - `True` and a success message if the unstake operation succeeded; + - `False` and an error message otherwise. + """ + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False, unlock.message + + call_params = { + "hotkey": hotkey, + "netuid": netuid, + "limit_price": None, + } + + if rate_tolerance: + current_price = (await subtensor.subnet(netuid=netuid)).price + limit_price = current_price * (1 - rate_tolerance) + call_params.update({"limit_price": limit_price}) + + async with subtensor.substrate as substrate: + call = await substrate.compose_call( + call_module="SubtensorModule", + call_function="remove_stake_full_limit", + call_params=call_params, + ) + + return await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + nonce_key="coldkeypub", + sign_with="coldkey", + use_nonce=True, + period=period, + ) + + async def unstake_multiple_extrinsic( subtensor: "AsyncSubtensor", wallet: "Wallet", @@ -214,23 +280,25 @@ async def unstake_multiple_extrinsic( """Removes stake from each ``hotkey_ss58`` in the list, using each amount, to a common coldkey. Args: - subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance. - wallet (bittensor_wallet.Wallet): The wallet with the coldkey to unstake to. - hotkey_ss58s (List[str]): List of hotkeys to unstake from. - netuids (List[int]): List of netuids to unstake from. - amounts (List[Union[Balance, float]]): List of amounts to unstake. If ``None``, unstake all. - wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``True``, or + subtensor: Subtensor instance. + wallet: The wallet with the coldkey to unstake to. + hotkey_ss58s: List of hotkeys to unstake from. + netuids: List of netuids to unstake from. + amounts: List of amounts to unstake. If ``None``, unstake all. + wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning ``True``, or returns ``False`` if the extrinsic fails to enter the block within the timeout. - wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning + wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning ``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout. - period (Optional[int]): 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. + 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. unstake_all: If true, unstakes all tokens. Default is ``False``. Returns: - success (bool): Flag is ``True`` if extrinsic was finalized or included in the block. Flag is ``True`` if any - wallet was unstaked. If we did not wait for finalization / inclusion, the response is ``True``. + tuple[bool, str]: + A tuple containing: + - `True` and a success message if the unstake operation succeeded; + - `False` and an error message otherwise. """ if amounts and unstake_all: raise ValueError("Cannot specify both `amounts` and `unstake_all`.") diff --git a/bittensor/core/extrinsics/transfer.py b/bittensor/core/extrinsics/transfer.py index a2ac8df11d..370efaed36 100644 --- a/bittensor/core/extrinsics/transfer.py +++ b/bittensor/core/extrinsics/transfer.py @@ -143,7 +143,7 @@ def transfer_extrinsic( logging.error(f"\t\tFor fee:\t[blue]{fee}[/blue]") return False - logging.info(":satellite: [magenta]Transferring... tuple[bool, str]: + """Unstakes all TAO/Alpha associated with a hotkey from the specified subnets on the Bittensor network. + + Arguments: + subtensor: Subtensor instance. + wallet: The wallet of the stake owner. + hotkey: The SS58 address of the hotkey to unstake from. + netuid: The unique identifier of the subnet. + rate_tolerance: The maximum allowed price change ratio when unstaking. For example, 0.005 = 0.5% maximum + price decrease. If not passed (None), then unstaking goes without price limit. Default is `0.005`. + wait_for_inclusion: Waits for the transaction to be included in a block. Default is `True`. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. Default is `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. Default is `None`. + + Returns: + tuple[bool, str]: + A tuple containing: + - `True` and a success message if the unstake operation succeeded; + - `False` and an error message otherwise. + """ + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False, unlock.message + + call_params = { + "hotkey": hotkey, + "netuid": netuid, + "limit_price": None, + } + + if rate_tolerance: + current_price = subtensor.subnet(netuid=netuid).price + limit_price = current_price * (1 - rate_tolerance) + call_params.update({"limit_price": limit_price}) + + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="remove_stake_full_limit", + call_params=call_params, + ) + + success, message = subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + nonce_key="coldkeypub", + sign_with="coldkey", + use_nonce=True, + period=period, + ) + + return success, message + + def unstake_multiple_extrinsic( subtensor: "Subtensor", wallet: "Wallet", @@ -211,23 +278,25 @@ def unstake_multiple_extrinsic( """Removes stake from each ``hotkey_ss58`` in the list, using each amount, to a common coldkey. Args: - subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance. - wallet (bittensor_wallet.Wallet): The wallet with the coldkey to unstake to. - hotkey_ss58s (List[str]): List of hotkeys to unstake from. - netuids (List[int]): List of subnets unique IDs to unstake from. - amounts (List[Balance]): List of amounts to unstake. If ``None``, unstake all. - wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``True``, or + subtensor: Subtensor instance. + wallet: The wallet with the coldkey to unstake to. + hotkey_ss58s: List of hotkeys to unstake from. + netuids: List of subnets unique IDs to unstake from. + amounts: List of amounts to unstake. If ``None``, unstake all. + wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning ``True``, or returns ``False`` if the extrinsic fails to enter the block within the timeout. - wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning - ``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout. - period (Optional[int]): 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. + wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning ``True``, + or returns ``False`` if the extrinsic fails to be finalized within the timeout. + 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. unstake_all: If true, unstakes all tokens. Default is ``False``. Returns: - success (bool): Flag is ``True`` if extrinsic was finalized or included in the block. Flag is ``True`` if any - wallet was unstaked. If we did not wait for finalization / inclusion, the response is ``True``. + tuple[bool, str]: + A tuple containing: + - `True` and a success message if the unstake operation succeeded; + - `False` and an error message otherwise. """ if amounts and unstake_all: raise ValueError("Cannot specify both `amounts` and `unstake_all`.") diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index eb608ab87f..ab01d80317 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -79,6 +79,7 @@ ) from bittensor.core.extrinsics.transfer import transfer_extrinsic from bittensor.core.extrinsics.unstaking import ( + unstake_all_extrinsic, unstake_extrinsic, unstake_multiple_extrinsic, ) @@ -3798,14 +3799,13 @@ def unstake( individual neuron stakes within the Bittensor network. Args: - wallet (bittensor_wallet.wallet): The wallet associated with the neuron from which the stake is being - removed. - hotkey_ss58 (Optional[str]): The ``SS58`` address of the hotkey account to unstake from. - netuid (Optional[int]): The unique identifier of the subnet. - amount (Balance): The amount of alpha to unstake. If not specified, unstakes all. Alpha amount. - wait_for_inclusion (bool): Waits for the transaction to be included in a block. - wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. - safe_staking (bool): If true, enables price safety checks to protect against fluctuating prices. The unstake + wallet: The wallet associated with the neuron from which the stake is being removed. + hotkey_ss58: The ``SS58`` address of the hotkey account to unstake from. + netuid: The unique identifier of the subnet. + amount: The amount of alpha to unstake. If not specified, unstakes all. Alpha amount. + wait_for_inclusion: Waits for the transaction to be included in a block. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. + safe_staking: If true, enables price safety checks to protect against fluctuating prices. The unstake will only execute if the price change doesn't exceed the rate tolerance. Default is False. allow_partial_stake (bool): If true and safe_staking is enabled, allows partial unstaking when the full amount would exceed the price tolerance. If false, the entire unstake fails if it would @@ -3840,6 +3840,89 @@ def unstake( unstake_all=unstake_all, ) + def unstake_all( + self, + wallet: "Wallet", + hotkey: str, + netuid: int, + rate_tolerance: Optional[float] = 0.005, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + period: Optional[int] = None, + ) -> tuple[bool, str]: + """Unstakes all TAO/Alpha associated with a hotkey from the specified subnets on the Bittensor network. + + Arguments: + wallet: The wallet of the stake owner. + hotkey: The SS58 address of the hotkey to unstake from. + netuid: The unique identifier of the subnet. + rate_tolerance: The maximum allowed price change ratio when unstaking. For example, 0.005 = 0.5% maximum + price decrease. If not passed (None), then unstaking goes without price limit. Default is 0.005. + wait_for_inclusion: Waits for the transaction to be included in a block. Default is `True`. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. Default is `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. Default is `None`. + + Returns: + tuple[bool, str]: + A tuple containing: + - `True` and a success message if the unstake operation succeeded; + - `False` and an error message otherwise. + + Example: + # If you would like to unstake all stakes in all subnets safely: + import bittensor as bt + + subtensor = bt.Subtensor() + wallet = bt.Wallet("my_wallet") + netuid = 14 + hotkey = "5%SOME_HOTKEY%" + + wallet_stakes = subtensor.get_stake_info_for_coldkey(coldkey_ss58=wallet.coldkey.ss58_address) + + for stake in wallet_stakes: + result = subtensor.unstake_all( + wallet=wallet, + hotkey_ss58=stake.hotkey_ss58, + netuid=stake.netuid, + ) + print(result) + + # If you would like to unstake all stakes in all subnets unsafely, use `rate_tolerance=None`: + import bittensor as bt + + subtensor = bt.AsyncSubtensor() + wallet = bt.Wallet("my_wallet") + netuid = 14 + hotkey = "5%SOME_HOTKEY_WHERE_IS_YOUR_STAKE_NOW%" + + wallet_stakes = await subtensor.get_stake_info_for_coldkey(coldkey_ss58=wallet.coldkey.ss58_address) + + for stake in wallet_stakes: + result = await subtensor.unstake_all( + wallet=wallet, + hotkey_ss58=stake.hotkey_ss58, + netuid=stake.netuid, + rate_tolerance=None, + ) + print(result) + """ + if netuid != 0: + logging.debug( + f"Unstaking without Alpha price control from subnet [blue]#{netuid}[/blue]." + ) + return unstake_all_extrinsic( + subtensor=self, + wallet=wallet, + hotkey=hotkey, + netuid=netuid, + rate_tolerance=rate_tolerance, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + ) + def unstake_multiple( self, wallet: "Wallet", @@ -3856,7 +3939,7 @@ def unstake_multiple( efficiently. This function is useful for managing the distribution of stakes across multiple neurons. Args: - wallet (bittensor_wallet.Wallet): The wallet linked to the coldkey from which the stakes are being + wallet: The wallet linked to the coldkey from which the stakes are being withdrawn. hotkey_ss58s (List[str]): A list of hotkey ``SS58`` addresses to unstake from. netuids (List[int]): The list of subnet uids. diff --git a/bittensor/core/subtensor_api/staking.py b/bittensor/core/subtensor_api/staking.py index 6ccce7fd4d..b0e8d3d472 100644 --- a/bittensor/core/subtensor_api/staking.py +++ b/bittensor/core/subtensor_api/staking.py @@ -21,4 +21,5 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.get_stake_movement_fee = subtensor.get_stake_movement_fee self.get_unstake_fee = subtensor.get_unstake_fee self.unstake = subtensor.unstake + self.unstake_all = subtensor.unstake_all self.unstake_multiple = subtensor.unstake_multiple diff --git a/bittensor/core/subtensor_api/utils.py b/bittensor/core/subtensor_api/utils.py index 30afcce7f5..ecb30be73a 100644 --- a/bittensor/core/subtensor_api/utils.py +++ b/bittensor/core/subtensor_api/utils.py @@ -161,6 +161,7 @@ def add_legacy_methods(subtensor: "SubtensorApi"): subtensor.transfer_stake = subtensor._subtensor.transfer_stake subtensor.tx_rate_limit = subtensor._subtensor.tx_rate_limit subtensor.unstake = subtensor._subtensor.unstake + subtensor.unstake_all = subtensor._subtensor.unstake_all subtensor.unstake_multiple = subtensor._subtensor.unstake_multiple subtensor.wait_for_block = subtensor._subtensor.wait_for_block subtensor.weights = subtensor._subtensor.weights diff --git a/tests/e2e_tests/test_staking.py b/tests/e2e_tests/test_staking.py index fdf874b285..086c570490 100644 --- a/tests/e2e_tests/test_staking.py +++ b/tests/e2e_tests/test_staking.py @@ -1,3 +1,6 @@ +import pytest + +from bittensor.core.errors import ChainError from bittensor import logging from bittensor.core.chain_data.stake_info import StakeInfo from bittensor.utils.balance import Balance @@ -830,3 +833,141 @@ def test_transfer_stake(subtensor, alice_wallet, bob_wallet, dave_wallet): ] assert bob_stakes == expected_bob_stake logging.console.success("✅ Test [green]test_transfer_stake[/green] passed") + + +# For test we set rate_tolerance=0.7 (70%) because of price is highly dynamic for fast-blocks and 2 SN to avoid ` +# Slippage is too high for the transaction`. This logic controls by the chain. +# Also this test implementation works with non-fast-blocks run. +@pytest.mark.parametrize( + "rate_tolerance", + [None, 1.0], + ids=[ + "Without price limit", + "With price limit", + ], +) +def test_unstaking_with_limit( + subtensor, alice_wallet, bob_wallet, dave_wallet, rate_tolerance +): + """Test unstaking with limits goes well for all subnets with and without price limit.""" + + # Register first SN + alice_subnet_netuid_2 = subtensor.get_total_subnets() # 2 + assert subtensor.register_subnet(alice_wallet, True, True) + assert subtensor.subnet_exists(alice_subnet_netuid_2), ( + "Subnet wasn't created successfully" + ) + + wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid_2) + + assert subtensor.start_call( + alice_wallet, + netuid=alice_subnet_netuid_2, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + # Register Bob and Dave in SN2 + assert subtensor.burned_register( + wallet=bob_wallet, + netuid=alice_subnet_netuid_2, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + assert subtensor.burned_register( + wallet=dave_wallet, + netuid=alice_subnet_netuid_2, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + # Register second SN + alice_subnet_netuid_3 = subtensor.get_total_subnets() # 3 + assert subtensor.register_subnet(alice_wallet, True, True) + assert subtensor.subnet_exists(alice_subnet_netuid_3), ( + "Subnet wasn't created successfully" + ) + + wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid_3) + + assert subtensor.start_call( + alice_wallet, + netuid=alice_subnet_netuid_3, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + # Register Bob and Dave in SN3 + assert subtensor.burned_register( + wallet=bob_wallet, + netuid=alice_subnet_netuid_3, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + assert subtensor.burned_register( + wallet=dave_wallet, + netuid=alice_subnet_netuid_3, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + # Check Bob's stakes are empty. + assert subtensor.get_stake_info_for_coldkey(bob_wallet.coldkey.ss58_address) == [] + + # Bob stakes to Dave in both SNs + + assert subtensor.add_stake( + wallet=bob_wallet, + hotkey_ss58=dave_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid_2, + amount=Balance.from_tao(10000), + wait_for_inclusion=True, + wait_for_finalization=True, + period=16, + ), f"Cant add stake to dave in SN {alice_subnet_netuid_2}" + assert subtensor.add_stake( + wallet=bob_wallet, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid_3, + amount=Balance.from_tao(15000), + wait_for_inclusion=True, + wait_for_finalization=True, + period=16, + ), f"Cant add stake to dave in SN {alice_subnet_netuid_3}" + + # Check that both stakes are presented in result + bob_stakes = subtensor.get_stake_info_for_coldkey(bob_wallet.coldkey.ss58_address) + assert len(bob_stakes) == 2 + + if rate_tolerance == 0.0001: + # Raise the error + with pytest.raises( + ChainError, match="Slippage is too high for the transaction" + ): + subtensor.unstake_all( + wallet=bob_wallet, + hotkey=bob_stakes[0].hotkey_ss58, + netuid=bob_stakes[0].netuid, + rate_tolerance=rate_tolerance, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + else: + # Successful cases + for si in bob_stakes: + assert subtensor.unstake_all( + wallet=bob_wallet, + hotkey=si.hotkey_ss58, + netuid=si.netuid, + rate_tolerance=rate_tolerance, + wait_for_inclusion=True, + wait_for_finalization=True, + )[0] + + # Make sure both unstake were successful. + bob_stakes = subtensor.get_stake_info_for_coldkey( + bob_wallet.coldkey.ss58_address + ) + assert len(bob_stakes) == 0 diff --git a/tests/unit_tests/extrinsics/asyncex/test_unstaking.py b/tests/unit_tests/extrinsics/asyncex/test_unstaking.py new file mode 100644 index 0000000000..ed74c76fe4 --- /dev/null +++ b/tests/unit_tests/extrinsics/asyncex/test_unstaking.py @@ -0,0 +1,172 @@ +import pytest + +from bittensor.core.extrinsics.asyncex import unstaking +from bittensor.utils.balance import Balance + + +@pytest.mark.asyncio +async def test_unstake_extrinsic(fake_wallet, mocker): + # Preps + fake_subtensor = mocker.AsyncMock( + **{ + "get_hotkey_owner.return_value": "hotkey_owner", + "get_stake_for_coldkey_and_hotkey.return_value": Balance(10.0), + "sign_and_send_extrinsic.return_value": (True, ""), + "get_stake.return_value": Balance(10.0), + } + ) + + fake_wallet.coldkeypub.ss58_address = "hotkey_owner" + hotkey_ss58 = "hotkey" + fake_netuid = 1 + amount = Balance.from_tao(1.1) + wait_for_inclusion = True + wait_for_finalization = True + + # Call + result = await unstaking.unstake_extrinsic( + subtensor=fake_subtensor, + wallet=fake_wallet, + hotkey_ss58=hotkey_ss58, + netuid=fake_netuid, + amount=amount, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + # Asserts + assert result is True + + fake_subtensor.substrate.compose_call.assert_awaited_once_with( + call_module="SubtensorModule", + call_function="remove_stake", + call_params={ + "hotkey": "hotkey", + "amount_unstaked": 1100000000, + "netuid": 1, + }, + ) + fake_subtensor.sign_and_send_extrinsic.assert_awaited_once_with( + call=fake_subtensor.substrate.compose_call.return_value, + wallet=fake_wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + sign_with="coldkey", + nonce_key="coldkeypub", + use_nonce=True, + period=None, + ) + + +@pytest.mark.asyncio +async def test_unstake_all_extrinsic(fake_wallet, mocker): + # Preps + fake_subtensor = mocker.AsyncMock( + **{ + "subnet.return_value": mocker.Mock(price=100), + "sign_and_send_extrinsic.return_value": (True, ""), + } + ) + fake_substrate = fake_subtensor.substrate.__aenter__.return_value + hotkey = "hotkey" + fake_netuid = 1 + + # Call + result = await unstaking.unstake_all_extrinsic( + subtensor=fake_subtensor, + wallet=fake_wallet, + hotkey=hotkey, + netuid=fake_netuid, + ) + + # Asserts + assert result[0] is True + assert result[1] == "" + + fake_substrate.compose_call.assert_awaited_once_with( + call_module="SubtensorModule", + call_function="remove_stake_full_limit", + call_params={ + "hotkey": "hotkey", + "netuid": fake_netuid, + "limit_price": 100 * (1 - 0.005), + }, + ) + fake_subtensor.sign_and_send_extrinsic.assert_awaited_once_with( + call=fake_substrate.compose_call.return_value, + wallet=fake_wallet, + wait_for_inclusion=True, + wait_for_finalization=False, + sign_with="coldkey", + nonce_key="coldkeypub", + use_nonce=True, + period=None, + ) + + +@pytest.mark.asyncio +async def test_unstake_multiple_extrinsic(fake_wallet, mocker): + """Verify that sync `unstake_multiple_extrinsic` method calls proper async method.""" + # Preps + fake_subtensor = mocker.AsyncMock( + **{ + "get_hotkey_owner.return_value": "hotkey_owner", + "get_stake_for_coldkey_and_hotkey.return_value": [Balance(10.0)], + "sign_and_send_extrinsic.return_value": (True, ""), + "tx_rate_limit.return_value": 0, + } + ) + mocker.patch.object( + unstaking, "get_old_stakes", return_value=[Balance(1.1), Balance(0.3)] + ) + fake_wallet.coldkeypub.ss58_address = "hotkey_owner" + hotkey_ss58s = ["hotkey1", "hotkey2"] + fake_netuids = [1, 2] + amounts = [Balance.from_tao(1.1), Balance.from_tao(1.2)] + wait_for_inclusion = True + wait_for_finalization = True + + # Call + result = await unstaking.unstake_multiple_extrinsic( + subtensor=fake_subtensor, + wallet=fake_wallet, + hotkey_ss58s=hotkey_ss58s, + netuids=fake_netuids, + amounts=amounts, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + # Asserts + assert result is True + assert fake_subtensor.substrate.compose_call.call_count == 1 + assert fake_subtensor.sign_and_send_extrinsic.call_count == 1 + + fake_subtensor.substrate.compose_call.assert_any_call( + call_module="SubtensorModule", + call_function="remove_stake", + call_params={ + "hotkey": "hotkey1", + "amount_unstaked": 1100000000, + "netuid": 1, + }, + ) + fake_subtensor.substrate.compose_call.assert_any_call( + call_module="SubtensorModule", + call_function="remove_stake", + call_params={ + "hotkey": "hotkey1", + "amount_unstaked": 1100000000, + "netuid": 1, + }, + ) + fake_subtensor.sign_and_send_extrinsic.assert_awaited_with( + call=fake_subtensor.substrate.compose_call.return_value, + wallet=fake_wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + sign_with="coldkey", + nonce_key="coldkeypub", + use_nonce=True, + period=None, + ) diff --git a/tests/unit_tests/extrinsics/test_unstaking.py b/tests/unit_tests/extrinsics/test_unstaking.py index 2fdf0cbe47..04b93111d2 100644 --- a/tests/unit_tests/extrinsics/test_unstaking.py +++ b/tests/unit_tests/extrinsics/test_unstaking.py @@ -54,6 +54,51 @@ def test_unstake_extrinsic(fake_wallet, mocker): ) +def test_unstake_all_extrinsic(fake_wallet, mocker): + # Preps + fake_subtensor = mocker.Mock( + **{ + "subnet.return_value": mocker.Mock(price=100), + "sign_and_send_extrinsic.return_value": (True, ""), + } + ) + + hotkey = "hotkey" + fake_netuid = 1 + + # Call + result = unstaking.unstake_all_extrinsic( + subtensor=fake_subtensor, + wallet=fake_wallet, + hotkey=hotkey, + netuid=fake_netuid, + ) + + # Asserts + assert result[0] is True + assert result[1] == "" + + fake_subtensor.substrate.compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="remove_stake_full_limit", + call_params={ + "hotkey": "hotkey", + "netuid": fake_netuid, + "limit_price": 100 * (1 - 0.005), + }, + ) + fake_subtensor.sign_and_send_extrinsic.assert_called_once_with( + call=fake_subtensor.substrate.compose_call.return_value, + wallet=fake_wallet, + wait_for_inclusion=True, + wait_for_finalization=False, + sign_with="coldkey", + nonce_key="coldkeypub", + use_nonce=True, + period=None, + ) + + def test_unstake_multiple_extrinsic(fake_wallet, mocker): """Verify that sync `unstake_multiple_extrinsic` method calls proper async method.""" # Preps diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index ff537e800d..4feda5acab 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -3490,3 +3490,31 @@ 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_unstake_all(subtensor, fake_wallet, mocker): + """Verifies unstake_all calls properly.""" + # Preps + fake_unstake_all_extrinsic = mocker.AsyncMock() + mocker.patch.object( + async_subtensor, "unstake_all_extrinsic", fake_unstake_all_extrinsic + ) + # Call + result = await subtensor.unstake_all( + wallet=fake_wallet, + hotkey=fake_wallet.hotkey.ss58_address, + netuid=1, + ) + # Asserts + fake_unstake_all_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + wallet=fake_wallet, + hotkey=fake_wallet.hotkey.ss58_address, + netuid=1, + rate_tolerance=0.005, + wait_for_inclusion=True, + wait_for_finalization=False, + period=None, + ) + assert result == fake_unstake_all_extrinsic.return_value diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 0704fc16ab..83fcdcfa37 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -3875,3 +3875,30 @@ def test_set_children(subtensor, fake_wallet, mocker): period=None, ) assert result == mocked_set_children_extrinsic.return_value + + +def test_unstake_all(subtensor, fake_wallet, mocker): + """Verifies unstake_all calls properly.""" + # Preps + fake_unstake_all_extrinsic = mocker.Mock() + mocker.patch.object( + subtensor_module, "unstake_all_extrinsic", fake_unstake_all_extrinsic + ) + # Call + result = subtensor.unstake_all( + wallet=fake_wallet, + hotkey=fake_wallet.hotkey.ss58_address, + netuid=1, + ) + # Asserts + fake_unstake_all_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + hotkey=fake_wallet.hotkey.ss58_address, + netuid=1, + rate_tolerance=0.005, + wait_for_inclusion=True, + wait_for_finalization=False, + period=None, + ) + assert result == fake_unstake_all_extrinsic.return_value