From 9aa4b450d122997277b2c3801b4f4e488d05b350 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 25 Aug 2025 18:20:02 -0700 Subject: [PATCH 1/7] Use CamelCase in the name `CLOSE_IN_VALUE` --- tests/helpers/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/helpers/__init__.py b/tests/helpers/__init__.py index 624c3a08d7..9097d72d70 100644 --- a/tests/helpers/__init__.py +++ b/tests/helpers/__init__.py @@ -1,6 +1,6 @@ import os from .helpers import ( # noqa: F401 - CLOSE_IN_VALUE, + CloseInValue, __mock_wallet_factory__, ) from bittensor_wallet.mock.wallet_mock import ( # noqa: F401 From 2b1789ef8546571413fc455ac699d82ef5ff6221 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 25 Aug 2025 18:20:37 -0700 Subject: [PATCH 2/7] add `move_all_stake` parameter to `move_stake_extrinsic` extrinsic --- .../core/extrinsics/asyncex/move_stake.py | 39 ++++++++++------ bittensor/core/extrinsics/move_stake.py | 45 ++++++++++++------- 2 files changed, 53 insertions(+), 31 deletions(-) diff --git a/bittensor/core/extrinsics/asyncex/move_stake.py b/bittensor/core/extrinsics/asyncex/move_stake.py index 8ed6a29141..d52f86f868 100644 --- a/bittensor/core/extrinsics/asyncex/move_stake.py +++ b/bittensor/core/extrinsics/asyncex/move_stake.py @@ -305,28 +305,34 @@ async def move_stake_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, period: Optional[int] = None, + move_all_stake: bool = False, ) -> bool: """ Moves stake from one hotkey to another within subnets in the Bittensor network. Args: - subtensor (Subtensor): The subtensor instance to interact with the blockchain. - wallet (Wallet): The wallet containing the coldkey to authorize the move. - origin_hotkey (str): SS58 address of the origin hotkey associated with the stake. - origin_netuid (int): Network UID of the origin subnet. - destination_hotkey (str): SS58 address of the destination hotkey. - destination_netuid (int): Network UID of the destination subnet. - amount (Balance): The amount of stake to move as a `Balance` object. - wait_for_inclusion (bool): If True, waits for transaction inclusion in a block. Defaults to True. - wait_for_finalization (bool): If True, waits for transaction finalization. Defaults to False. - 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. + subtensor: The subtensor instance to interact with the blockchain. + wallet: The wallet containing the coldkey to authorize the move. + origin_hotkey: SS58 address of the origin hotkey associated with the stake. + origin_netuid: Network UID of the origin subnet. + destination_hotkey: SS58 address of the destination hotkey. + destination_netuid: Network UID of the destination subnet. + amount: The amount of stake to move as a `Balance` object. + wait_for_inclusion: If True, waits for transaction inclusion in a block. Defaults to True. + wait_for_finalization: If True, waits for transaction finalization. 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. + move_all_stake: If true, moves all stake from the source hotkey to the destination hotkey. Returns: bool: True if the move was successful, False otherwise. """ - amount.set_unit(netuid=origin_netuid) + if not amount and not move_all_stake: + logging.error( + ":cross_mark: [red]Failed[/red]: Please specify an `amount` or `move_all_stake` argument to move stake." + ) + return False # Check sufficient stake stake_in_origin, stake_in_destination = await _get_stake_in_origin_and_dest( @@ -338,13 +344,18 @@ async def move_stake_extrinsic( origin_netuid=origin_netuid, destination_netuid=destination_netuid, ) - if stake_in_origin < amount: + if move_all_stake: + amount = stake_in_origin + + elif stake_in_origin < amount: logging.error( f":cross_mark: [red]Failed[/red]: Insufficient stake in origin hotkey: {origin_hotkey}. " f"Stake: {stake_in_origin}, amount: {amount}" ) return False + amount.set_unit(netuid=origin_netuid) + try: logging.info( f"Moving stake from hotkey [blue]{origin_hotkey}[/blue] to hotkey [blue]{destination_hotkey}[/blue]\n" diff --git a/bittensor/core/extrinsics/move_stake.py b/bittensor/core/extrinsics/move_stake.py index d3874f1e68..f3aae363ce 100644 --- a/bittensor/core/extrinsics/move_stake.py +++ b/bittensor/core/extrinsics/move_stake.py @@ -301,29 +301,34 @@ def move_stake_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, period: Optional[int] = None, + move_all_stake: bool = False, ) -> bool: """ Moves stake to a different hotkey and/or subnet while keeping the same coldkey owner. Args: - subtensor (Subtensor): Subtensor instance. - wallet (bittensor.wallet): The wallet to move stake from. - origin_hotkey (str): The SS58 address of the source hotkey. - origin_netuid (int): The netuid of the source subnet. - destination_hotkey (str): The SS58 address of the destination hotkey. - destination_netuid (int): The netuid of the destination subnet. - amount (Union[Balance, float]): Amount to move. - wait_for_inclusion (bool): If true, waits for inclusion before returning. - wait_for_finalization (bool): If true, waits for finalization before returning. - 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. + subtensor: Subtensor instance. + wallet: The wallet to move stake from. + origin_hotkey: The SS58 address of the source hotkey. + origin_netuid: The netuid of the source subnet. + destination_hotkey: The SS58 address of the destination hotkey. + destination_netuid: The netuid of the destination subnet. + amount: Amount to move. + wait_for_inclusion: If true, waits for inclusion before returning. + wait_for_finalization: If true, waits for finalization before returning. + 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. + move_all_stake: If true, moves all stake from the source hotkey to the destination hotkey. Returns: - success (bool): True if the move was successful. + success: True if the move was successful. Otherwise, False. """ - - amount.set_unit(netuid=origin_netuid) + if not amount and not move_all_stake: + logging.error( + ":cross_mark: [red]Failed[/red]: Please specify an `amount` or `move_all_stake` argument to move stake." + ) + return False # Check sufficient stake stake_in_origin, stake_in_destination = _get_stake_in_origin_and_dest( @@ -335,12 +340,18 @@ def move_stake_extrinsic( origin_coldkey_ss58=wallet.coldkeypub.ss58_address, destination_coldkey_ss58=wallet.coldkeypub.ss58_address, ) - if stake_in_origin < amount: + if move_all_stake: + amount = stake_in_origin + + elif stake_in_origin < amount: logging.error( - f":cross_mark: [red]Failed[/red]: Insufficient stake in origin hotkey: {origin_hotkey}. Stake: {stake_in_origin}, amount: {amount}" + f":cross_mark: [red]Failed[/red]: Insufficient stake in origin hotkey: {origin_hotkey}. " + f"Stake: {stake_in_origin}, amount: {amount}" ) return False + amount.set_unit(netuid=origin_netuid) + try: logging.info( f"Moving stake from hotkey [blue]{origin_hotkey}[/blue] to hotkey [blue]{destination_hotkey}[/blue]\n" From c4c93d59a035687ed917ff3df4c777be02dd2621 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 25 Aug 2025 18:21:01 -0700 Subject: [PATCH 3/7] update `move_stake` methods in Subtensor class --- bittensor/core/async_subtensor.py | 5 ++++- bittensor/core/subtensor.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 6665f9d9e4..e40b1f4465 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -4657,10 +4657,11 @@ async def move_stake( origin_netuid: int, destination_hotkey: str, destination_netuid: int, - amount: Balance, + amount: Optional[Balance] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, period: Optional[int] = None, + move_all_stake: bool = False, ) -> bool: """ Moves stake to a different hotkey and/or subnet. @@ -4677,6 +4678,7 @@ async def move_stake( 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. + move_all_stake: If true, moves all stake from the source hotkey to the destination hotkey. Returns: success: True if the stake movement was successful. @@ -4693,6 +4695,7 @@ async def move_stake( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, period=period, + move_all_stake=move_all_stake, ) async def register( diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 177ce99e09..28371fb7d6 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -3493,10 +3493,11 @@ def move_stake( origin_netuid: int, destination_hotkey: str, destination_netuid: int, - amount: Balance, + amount: Optional[Balance] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, period: Optional[int] = None, + move_all_stake: bool = False, ) -> bool: """ Moves stake to a different hotkey and/or subnet. @@ -3513,6 +3514,7 @@ def move_stake( 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. + move_all_stake: If true, moves all stake from the source hotkey to the destination hotkey. Returns: success (bool): True if the stake movement was successful. @@ -3529,6 +3531,7 @@ def move_stake( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, period=period, + move_all_stake=move_all_stake, ) def register( From f4526990ffed45593d9261e3043837624ce24dac Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 25 Aug 2025 18:21:20 -0700 Subject: [PATCH 4/7] update SubtensorApi --- bittensor/core/subtensor_api/staking.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bittensor/core/subtensor_api/staking.py b/bittensor/core/subtensor_api/staking.py index 979d8a2632..40464e7ccd 100644 --- a/bittensor/core/subtensor_api/staking.py +++ b/bittensor/core/subtensor_api/staking.py @@ -22,6 +22,7 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.get_stake_operations_fee = subtensor.get_stake_operations_fee self.get_stake_weight = subtensor.get_stake_weight self.get_unstake_fee = subtensor.get_unstake_fee + self.move_stake = subtensor.move_stake self.unstake = subtensor.unstake self.unstake_all = subtensor.unstake_all self.unstake_multiple = subtensor.unstake_multiple From e1826b2cd506c38cab6773cfcabdea927cc8caea Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 25 Aug 2025 18:21:36 -0700 Subject: [PATCH 5/7] update helper --- tests/helpers/helpers.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/helpers/helpers.py b/tests/helpers/helpers.py index fed6638c52..eccf716811 100644 --- a/tests/helpers/helpers.py +++ b/tests/helpers/helpers.py @@ -34,7 +34,7 @@ def __mock_wallet_factory__(*_, **__) -> _MockWallet: return mock_wallet -class CLOSE_IN_VALUE: +class CloseInValue: value: Union[float, int, Balance] tolerance: Union[float, int, Balance] @@ -53,8 +53,14 @@ def __eq__(self, __o: Union[float, int, Balance]) -> bool: (self.value - self.tolerance) <= __o <= (self.value + self.tolerance) ) or ((__o - self.tolerance) <= self.value <= (__o + self.tolerance)) + def __str__(self) -> str: + return f"CloseInValue" -class ApproxBalance(CLOSE_IN_VALUE, Balance): + def __repr__(self) -> str: + return self.__str__() + + +class ApproxBalance(CloseInValue, Balance): def __init__( self, balance: Union[float, int], From 9a3cf94e5217d607675aaa0454ff1025d254e481 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 25 Aug 2025 18:21:56 -0700 Subject: [PATCH 6/7] refactoring --- tests/e2e_tests/test_delegate.py | 6 +++--- tests/unit_tests/utils/test_balance.py | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/e2e_tests/test_delegate.py b/tests/e2e_tests/test_delegate.py index 9fb4b9dd1a..949b1324be 100644 --- a/tests/e2e_tests/test_delegate.py +++ b/tests/e2e_tests/test_delegate.py @@ -13,7 +13,7 @@ vote, ) from tests.e2e_tests.utils.e2e_test_utils import wait_to_start_call -from tests.helpers.helpers import CLOSE_IN_VALUE +from tests.helpers.helpers import CloseInValue DEFAULT_DELEGATE_TAKE = 0.179995422293431 @@ -449,7 +449,7 @@ def test_get_vote_data(subtensor, alice_wallet): assert proposal == ProposalVoteData( ayes=[], - end=CLOSE_IN_VALUE(1_000_000, subtensor.block), + end=CloseInValue(1_000_000, subtensor.block), index=0, nays=[], threshold=3, @@ -475,7 +475,7 @@ def test_get_vote_data(subtensor, alice_wallet): ayes=[ alice_wallet.hotkey.ss58_address, ], - end=CLOSE_IN_VALUE(1_000_000, subtensor.block), + end=CloseInValue(1_000_000, subtensor.block), index=0, nays=[], threshold=3, diff --git a/tests/unit_tests/utils/test_balance.py b/tests/unit_tests/utils/test_balance.py index 43531d3721..4ff97bdb81 100644 --- a/tests/unit_tests/utils/test_balance.py +++ b/tests/unit_tests/utils/test_balance.py @@ -7,7 +7,7 @@ from hypothesis import strategies as st from bittensor.utils.balance import Balance -from tests.helpers import CLOSE_IN_VALUE +from tests.helpers import CloseInValue valid_tao_numbers_strategy = st.one_of( @@ -36,7 +36,7 @@ def test_balance_init(balance: Union[int, float]): if isinstance(balance, int): assert balance_.rao == balance elif isinstance(balance, float): - assert balance_.tao == CLOSE_IN_VALUE(balance, 0.00001) + assert balance_.tao == CloseInValue(balance, 0.00001) @given(balance=valid_tao_numbers_strategy, balance2=valid_tao_numbers_strategy) @@ -59,7 +59,7 @@ def test_balance_add(balance: Union[int, float], balance2: Union[int, float]): sum_ = balance_ + balance2_ assert isinstance(sum_, Balance) - assert CLOSE_IN_VALUE(sum_.rao, 5) == rao_ + rao2_ + assert CloseInValue(sum_.rao, 5) == rao_ + rao2_ @given(balance=valid_tao_numbers_strategy, balance2=valid_tao_numbers_strategy) @@ -82,7 +82,7 @@ def test_balance_add_other_not_balance( sum_ = balance_ + balance2_ assert isinstance(sum_, Balance) - assert CLOSE_IN_VALUE(sum_.rao, 5) == rao_ + rao2_ + assert CloseInValue(sum_.rao, 5) == rao_ + rao2_ @given(balance=valid_tao_numbers_strategy) @@ -95,7 +95,7 @@ def test_balance_eq_other_not_balance(balance: Union[int, float]): # convert balance2 to rao. This assumes balance2 is a rao value rao2_ = int(balance_.rao) - assert CLOSE_IN_VALUE(rao2_, 5) == balance_ + assert CloseInValue(rao2_, 5) == balance_ @given(balance=valid_tao_numbers_strategy, balance2=valid_tao_numbers_strategy) @@ -118,7 +118,7 @@ def test_balance_radd_other_not_balance( sum_ = balance2_ + balance_ # This is an radd assert isinstance(sum_, Balance) - assert CLOSE_IN_VALUE(sum_.rao, 5) == rao2_ + rao_ + assert CloseInValue(sum_.rao, 5) == rao2_ + rao_ @given(balance=valid_tao_numbers_strategy, balance2=valid_tao_numbers_strategy) @@ -141,7 +141,7 @@ def test_balance_sub(balance: Union[int, float], balance2: Union[int, float]): diff_ = balance_ - balance2_ assert isinstance(diff_, Balance) - assert CLOSE_IN_VALUE(diff_.rao, 5) == rao_ - rao2_ + assert CloseInValue(diff_.rao, 5) == rao_ - rao2_ @given(balance=valid_tao_numbers_strategy, balance2=valid_tao_numbers_strategy) @@ -164,7 +164,7 @@ def test_balance_sub_other_not_balance( diff_ = balance_ - balance2_ assert isinstance(diff_, Balance) - assert CLOSE_IN_VALUE(diff_.rao, 5) == rao_ - rao2_ + assert CloseInValue(diff_.rao, 5) == rao_ - rao2_ @given(balance=valid_tao_numbers_strategy, balance2=valid_tao_numbers_strategy) @@ -187,7 +187,7 @@ def test_balance_rsub_other_not_balance( diff_ = balance2_ - balance_ # This is an rsub assert isinstance(diff_, Balance) - assert CLOSE_IN_VALUE(diff_.rao, 5) == rao2_ - rao_ + assert CloseInValue(diff_.rao, 5) == rao2_ - rao_ @given(balance=valid_tao_numbers_strategy, balance2=valid_tao_numbers_strategy) @@ -373,7 +373,7 @@ def test_balance_floordiv(balance: Union[int, float], balance2: Union[int, float quot_ = balance_ // balance2_ assert isinstance(quot_, Balance) - assert CLOSE_IN_VALUE(quot_.rao, 5) == rao_ // rao2_ + assert CloseInValue(quot_.rao, 5) == rao_ // rao2_ @given( From c9415a5caf2ef09da1d2c7d56c3e5e2e4f4b0426 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 25 Aug 2025 18:22:46 -0700 Subject: [PATCH 7/7] extend e2e test to check `move_stake` method behavior with `move_all_stake=True` argument --- tests/e2e_tests/test_staking.py | 88 +++++++++++++++++++++++++++------ 1 file changed, 73 insertions(+), 15 deletions(-) diff --git a/tests/e2e_tests/test_staking.py b/tests/e2e_tests/test_staking.py index 0e18ff7b08..b0c21b6c22 100644 --- a/tests/e2e_tests/test_staking.py +++ b/tests/e2e_tests/test_staking.py @@ -9,7 +9,7 @@ sudo_set_admin_utils, ) from tests.e2e_tests.utils.e2e_test_utils import wait_to_start_call -from tests.helpers.helpers import CLOSE_IN_VALUE +from tests.helpers.helpers import CloseInValue def test_single_operation(subtensor, alice_wallet, bob_wallet): @@ -323,7 +323,7 @@ def test_batch_operations(subtensor, alice_wallet, bob_wallet): bob_wallet.coldkey.ss58_address, ) - assert CLOSE_IN_VALUE( # Make sure we are within 0.0001 TAO due to tx fees + assert CloseInValue( # Make sure we are within 0.0001 TAO due to tx fees balances[bob_wallet.coldkey.ss58_address], Balance.from_rao(100_000) ) == Balance.from_tao(999_999.7994) @@ -643,11 +643,12 @@ def test_safe_swap_stake_scenarios(subtensor, alice_wallet, bob_wallet): ) -def test_move_stake(subtensor, alice_wallet, bob_wallet): +def test_move_stake(subtensor, alice_wallet, bob_wallet, dave_wallet): """ Tests: - Adding stake - Moving stake from one hotkey-subnet pair to another + - Testing `move_stake` method with `move_all_stake=True` flag. """ alice_subnet_netuid = subtensor.get_total_subnets() # 2 @@ -658,16 +659,9 @@ def test_move_stake(subtensor, alice_wallet, bob_wallet): assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) - subtensor.burned_register( - alice_wallet, - netuid=alice_subnet_netuid, - wait_for_inclusion=True, - wait_for_finalization=True, - ) - assert subtensor.add_stake( - alice_wallet, - alice_wallet.hotkey.ss58_address, + wallet=alice_wallet, + hotkey_ss58=alice_wallet.hotkey.ss58_address, netuid=alice_subnet_netuid, amount=Balance.from_tao(1_000), wait_for_inclusion=True, @@ -697,8 +691,22 @@ def test_move_stake(subtensor, alice_wallet, bob_wallet): assert wait_to_start_call(subtensor, bob_wallet, bob_subnet_netuid) + subtensor.burned_register( + wallet=bob_wallet, + netuid=alice_subnet_netuid, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + subtensor.burned_register( + wallet=dave_wallet, + netuid=alice_subnet_netuid, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert subtensor.move_stake( - alice_wallet, + wallet=alice_wallet, origin_hotkey=alice_wallet.hotkey.ss58_address, origin_netuid=alice_subnet_netuid, destination_hotkey=bob_wallet.hotkey.ss58_address, @@ -743,9 +751,59 @@ def test_move_stake(subtensor, alice_wallet, bob_wallet): ) expected_stakes += fast_block_stake - assert stakes == expected_stakes - logging.console.success("✅ Test [green]test_move_stake[/green] passed") + + # test move_stake with move_all_stake=True + dave_stake = subtensor.staking.get_stake( + coldkey_ss58=dave_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=bob_subnet_netuid, + ) + logging.console.info(f"[orange]Dave stake before adding: {dave_stake}[orange]") + + assert subtensor.staking.add_stake( + wallet=dave_wallet, + hotkey_ss58=dave_wallet.hotkey.ss58_address, + netuid=bob_subnet_netuid, + amount=Balance.from_tao(1000), + wait_for_inclusion=True, + wait_for_finalization=True, + allow_partial_stake=True, + ) + + dave_stake = subtensor.staking.get_stake( + coldkey_ss58=dave_wallet.coldkey.ss58_address, + hotkey_ss58=dave_wallet.hotkey.ss58_address, + netuid=bob_subnet_netuid, + ) + logging.console.info(f"[orange]Dave stake after adding: {dave_stake}[orange]") + + # let chain to process the transaction + subtensor.wait_for_block( + subtensor.block + subtensor.subnets.tempo(netuid=bob_subnet_netuid) + ) + + assert subtensor.staking.move_stake( + wallet=dave_wallet, + origin_hotkey=dave_wallet.hotkey.ss58_address, + origin_netuid=bob_subnet_netuid, + destination_hotkey=bob_wallet.hotkey.ss58_address, + destination_netuid=bob_subnet_netuid, + wait_for_inclusion=True, + wait_for_finalization=True, + move_all_stake=True, + ) + + dave_stake = subtensor.staking.get_stake( + coldkey_ss58=dave_wallet.coldkey.ss58_address, + hotkey_ss58=dave_wallet.hotkey.ss58_address, + netuid=bob_subnet_netuid, + ) + logging.console.info(f"[orange]Dave stake after moving all: {dave_stake}[orange]") + + assert dave_stake.rao == CloseInValue(0, 0.00001) + + logging.console.success("✅ Test [green]test_move_stake[/green] passed.") def test_transfer_stake(subtensor, alice_wallet, bob_wallet, dave_wallet):