From cc957c045ef834bb7eafeebf9334d3c9a54be6c4 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Thu, 23 Oct 2025 10:19:52 -0700 Subject: [PATCH 01/21] improve liquidity list logic + e2e test + unit tests --- bittensor/core/async_subtensor.py | 40 ++++++++++++++---------- bittensor/core/subtensor.py | 19 ++++++----- tests/e2e_tests/test_liquidity.py | 3 +- tests/unit_tests/test_async_subtensor.py | 6 ++-- tests/unit_tests/test_subtensor.py | 7 +++-- 5 files changed, 44 insertions(+), 31 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 350fc01d52..e0085569ec 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -2423,6 +2423,17 @@ async def get_liquidity_list( logging.debug(f"Subnet {netuid} is not active.") return None + positions_response = await self.query_map( + module="Swap", + name="Positions", + params=[netuid, wallet.coldkeypub.ss58_address], + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + if len(positions_response.records) == 0: + return [] + block_hash = await self.determine_block_hash( block=block, block_hash=block_hash, reuse_block=reuse_block ) @@ -2446,25 +2457,20 @@ async def get_liquidity_list( params=[netuid], block_hash=block_hash, ) + ( - (fee_global_tao_query, fee_global_alpha_query, sqrt_price_query), - positions_response, - ) = await asyncio.gather( - self.substrate.query_multi( - [ - fee_global_tao_query_sk, - fee_global_alpha_query_sk, - sqrt_price_query_sk, - ], - block_hash=block_hash, - ), - self.query_map( - module="Swap", - name="Positions", - block=block, - params=[netuid, wallet.coldkeypub.ss58_address], - ), + fee_global_tao_query, + fee_global_alpha_query, + sqrt_price_query, + ) = await self.substrate.query_multi( + storage_keys=[ + fee_global_tao_query_sk, + fee_global_alpha_query_sk, + sqrt_price_query_sk, + ], + block_hash=block_hash, ) + # convert to floats fee_global_tao = fixed_to_float(fee_global_tao_query[1]) fee_global_alpha = fixed_to_float(fee_global_alpha_query[1]) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 2a1b4d317d..c12c6375f8 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1699,6 +1699,16 @@ def get_liquidity_list( logging.debug(f"Subnet {netuid} is not active.") return None + # Fetch positions + positions_response = self.query_map( + module="Swap", + name="Positions", + block=block, + params=[netuid, wallet.coldkeypub.ss58_address], + ) + if len(positions_response.records) == 0: + return [] + block_hash = self.determine_block_hash(block) # Fetch global fees and current price @@ -1722,7 +1732,7 @@ def get_liquidity_list( ) fee_global_tao_query, fee_global_alpha_query, sqrt_price_query = ( self.substrate.query_multi( - [ + storage_keys=[ fee_global_tao_query_sk, fee_global_alpha_query_sk, sqrt_price_query_sk, @@ -1736,13 +1746,6 @@ def get_liquidity_list( sqrt_price = fixed_to_float(sqrt_price_query[1]) 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_values: list[tuple[dict, int, int]] = [] positions_storage_keys: list[StorageKey] = [] for _, p in positions_response: diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py index 0abd694069..3408b5ab22 100644 --- a/tests/e2e_tests/test_liquidity.py +++ b/tests/e2e_tests/test_liquidity.py @@ -9,8 +9,7 @@ ) -@pytest.mark.asyncio -async def test_liquidity(subtensor, alice_wallet, bob_wallet): +def test_liquidity(subtensor, alice_wallet, bob_wallet): """ Tests the liquidity mechanism diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 295222f0ca..2ac95fb459 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -3492,7 +3492,7 @@ async def test_get_liquidity_list_happy_path(subtensor, fake_wallet, mocker): ], ] - fake_result = mocker.AsyncMock(autospec=list) + fake_result = mocker.AsyncMock(records=fake_positions, autospec=list) fake_result.__aiter__.return_value = iter(fake_positions) mocked_query_map = mocker.AsyncMock(return_value=fake_result) @@ -3512,8 +3512,10 @@ async def test_get_liquidity_list_happy_path(subtensor, fake_wallet, mocker): mocked_query_map.assert_awaited_once_with( module="Swap", name="Positions", - block=None, params=[netuid, fake_wallet.coldkeypub.ss58_address], + block=None, + block_hash=None, + reuse_block=False, ) assert len(result) == len(fake_positions) assert all([isinstance(p, async_subtensor.LiquidityPosition) for p in result]) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 5b7d7a626d..2ba605e219 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -3668,7 +3668,10 @@ def test_get_liquidity_list_happy_path(subtensor, fake_wallet, mocker): ), ], ] - mocked_query_map = mocker.MagicMock(return_value=fake_positions) + fake_result = mocker.MagicMock(records=fake_positions, autospec=list) + fake_result.__iter__.return_value = iter(fake_positions) + + mocked_query_map = mocker.Mock(return_value=fake_result) mocker.patch.object(subtensor, "query_map", new=mocked_query_map) # Mock storage key creation @@ -3713,8 +3716,8 @@ def test_get_liquidity_list_happy_path(subtensor, fake_wallet, mocker): mocked_query_map.assert_called_once_with( module="Swap", name="Positions", - block=None, params=[netuid, fake_wallet.coldkeypub.ss58_address], + block=None, ) assert mock_query_multi.call_count == 2 # one for fees, one for ticks assert len(result) == len(fake_positions) From eb397ac92be32085f1b591fe50b915faf82fa906 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 27 Oct 2025 10:27:36 -0700 Subject: [PATCH 02/21] improve setup_wallet --- tests/e2e_tests/utils/e2e_test_utils.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/e2e_tests/utils/e2e_test_utils.py b/tests/e2e_tests/utils/e2e_test_utils.py index 76e0916bdf..12a6abb29a 100644 --- a/tests/e2e_tests/utils/e2e_test_utils.py +++ b/tests/e2e_tests/utils/e2e_test_utils.py @@ -19,6 +19,7 @@ def setup_wallet( encrypt_hotkey: bool = False, coldkey_password: Optional[str] = None, hotkey_password: Optional[str] = None, + hk_uri: Optional[str] = None, ) -> tuple[Keypair, Wallet]: """ Sets up a wallet using the provided URI. @@ -30,25 +31,27 @@ def setup_wallet( - Creates a wallet in a temporary directory. - Sets keys in the wallet without encryption and with overwriting enabled. """ - keypair = Keypair.create_from_uri(uri) + keypair_ck = Keypair.create_from_uri(uri) + keypair_hk = Keypair.create_from_uri(f"{uri}_hk") if hk_uri else keypair_ck name = uri.strip("/") wallet_path = f"/tmp/btcli-e2e-wallet-{name}" wallet = Wallet(name=name, path=wallet_path) wallet.set_coldkey( - keypair=keypair, + keypair=keypair_ck, encrypt=encrypt_coldkey, overwrite=True, coldkey_password=coldkey_password, ) - wallet.set_coldkeypub(keypair=keypair, encrypt=False, overwrite=True) + wallet.set_coldkeypub(keypair=keypair_ck, encrypt=False, overwrite=True) wallet.set_hotkey( - keypair=keypair, + keypair=keypair_hk, encrypt=encrypt_hotkey, overwrite=True, hotkey_password=hotkey_password, ) - wallet.set_hotkeypub(keypair=keypair, encrypt=False, overwrite=True) - return keypair, wallet + wallet.set_hotkeypub(keypair=keypair_hk, encrypt=False, overwrite=True) + # TODO: return just wallet and update all usage places + return keypair_ck, wallet def clone_or_update_templates(specific_commit=None): From 0d1b635ffebb976ff075f1ad6e3a7b4f18d59b39 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 27 Oct 2025 10:28:54 -0700 Subject: [PATCH 03/21] update SubtensorApi --- bittensor/extras/subtensor_api/extrinsics.py | 2 ++ bittensor/extras/subtensor_api/staking.py | 3 +++ bittensor/extras/subtensor_api/utils.py | 1 + 3 files changed, 6 insertions(+) diff --git a/bittensor/extras/subtensor_api/extrinsics.py b/bittensor/extras/subtensor_api/extrinsics.py index 7ecf8aa1d5..69b3cde77f 100644 --- a/bittensor/extras/subtensor_api/extrinsics.py +++ b/bittensor/extras/subtensor_api/extrinsics.py @@ -11,6 +11,7 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.add_stake = subtensor.add_stake self.add_stake_multiple = subtensor.add_stake_multiple self.burned_register = subtensor.burned_register + self.claim_root = subtensor.claim_root self.commit_weights = subtensor.commit_weights self.contribute_crowdloan = subtensor.contribute_crowdloan self.create_crowdloan = subtensor.create_crowdloan @@ -33,6 +34,7 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.set_weights = subtensor.set_weights self.serve_axon = subtensor.serve_axon self.set_commitment = subtensor.set_commitment + self.set_root_claim_type = subtensor.set_root_claim_type self.start_call = subtensor.start_call self.swap_stake = subtensor.swap_stake self.toggle_user_liquidity = subtensor.toggle_user_liquidity diff --git a/bittensor/extras/subtensor_api/staking.py b/bittensor/extras/subtensor_api/staking.py index 12f798fca7..9e59beada6 100644 --- a/bittensor/extras/subtensor_api/staking.py +++ b/bittensor/extras/subtensor_api/staking.py @@ -9,9 +9,11 @@ class Staking: def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.add_stake = subtensor.add_stake self.add_stake_multiple = subtensor.add_stake_multiple + self.claim_root = subtensor.claim_root self.get_auto_stakes = subtensor.get_auto_stakes self.get_hotkey_stake = subtensor.get_hotkey_stake self.get_minimum_required_stake = subtensor.get_minimum_required_stake + self.get_root_claim_type = subtensor.get_root_claim_type self.get_stake = subtensor.get_stake self.get_stake_add_fee = subtensor.get_stake_add_fee self.get_stake_for_coldkey_and_hotkey = ( @@ -23,6 +25,7 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.get_unstake_fee = subtensor.get_unstake_fee self.move_stake = subtensor.move_stake self.set_auto_stake = subtensor.set_auto_stake + self.set_root_claim_type = subtensor.set_root_claim_type self.sim_swap = subtensor.sim_swap self.swap_stake = subtensor.swap_stake self.transfer_stake = subtensor.transfer_stake diff --git a/bittensor/extras/subtensor_api/utils.py b/bittensor/extras/subtensor_api/utils.py index ecc0d470f3..fce9fd97b1 100644 --- a/bittensor/extras/subtensor_api/utils.py +++ b/bittensor/extras/subtensor_api/utils.py @@ -109,6 +109,7 @@ def add_legacy_methods(subtensor: "SubtensorApi"): subtensor.get_revealed_commitment_by_hotkey = ( subtensor.inner_subtensor.get_revealed_commitment_by_hotkey ) + subtensor.get_root_claim_type = subtensor.inner_subtensor.get_root_claim_type subtensor.get_stake = subtensor.inner_subtensor.get_stake subtensor.get_stake_add_fee = subtensor.inner_subtensor.get_stake_add_fee subtensor.get_stake_for_coldkey_and_hotkey = ( From 829d7722e669761fb8acc13146d12be3c6ed3a28 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 27 Oct 2025 10:32:28 -0700 Subject: [PATCH 04/21] add sync extrinsic claim_root_extrinsic and set_root_claim_type_extrinsic --- bittensor/core/extrinsics/asyncex/root.py | 98 ++++++++++++++++++++++- bittensor/core/extrinsics/params/root.py | 8 ++ bittensor/core/extrinsics/root.py | 98 ++++++++++++++++++++++- 3 files changed, 202 insertions(+), 2 deletions(-) diff --git a/bittensor/core/extrinsics/asyncex/root.py b/bittensor/core/extrinsics/asyncex/root.py index 45ea91878e..58427eb157 100644 --- a/bittensor/core/extrinsics/asyncex/root.py +++ b/bittensor/core/extrinsics/asyncex/root.py @@ -1,5 +1,5 @@ import asyncio -from typing import Optional, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING, Literal from bittensor.core.extrinsics.params import RootParams from bittensor.core.types import ExtrinsicResponse @@ -142,3 +142,99 @@ async def root_register_extrinsic( except Exception as error: return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + +async def set_root_claim_type_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + new_root_claim_type: Literal["Swap", "Keep"], + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> ExtrinsicResponse: + """Sets the root claim type for the coldkey in provided wallet. + + Parameters: + subtensor: Subtensor instance to interact with the blockchain. + wallet: Bittensor Wallet instance. + new_root_claim_type: The new root claim type to set. Could be either "Swap" or "Keep". + 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. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + call = await subtensor.compose_call( + call_module="SubtensorModule", + call_function="set_root_claim_type", + call_params=RootParams.set_root_claim_type(new_root_claim_type), + ) + return await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + +async def claim_root_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> ExtrinsicResponse: + """Claims the root emissions for a coldkey. + + Parameters: + subtensor: Subtensor instance to interact with the blockchain. + wallet: Bittensor Wallet instance. + 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. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + call = await subtensor.compose_call( + call_module="SubtensorModule", + call_function="claim_root", + call_params={}, + ) + return await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) diff --git a/bittensor/core/extrinsics/params/root.py b/bittensor/core/extrinsics/params/root.py index add3cc8c50..f547c8a5d3 100644 --- a/bittensor/core/extrinsics/params/root.py +++ b/bittensor/core/extrinsics/params/root.py @@ -10,3 +10,11 @@ def root_register( ) -> dict: """Returns the parameters for the `root_register`.""" return {"hotkey": hotkey_ss58} + + @classmethod + def set_root_claim_type( + cls, + new_root_claim_type: str, + ) -> dict: + """Returns the parameters for the `set_root_claim_type`.""" + return {"new_root_claim_type": new_root_claim_type} diff --git a/bittensor/core/extrinsics/root.py b/bittensor/core/extrinsics/root.py index b924a60018..29a4539f9f 100644 --- a/bittensor/core/extrinsics/root.py +++ b/bittensor/core/extrinsics/root.py @@ -1,5 +1,5 @@ import time -from typing import Optional, TYPE_CHECKING +from typing import Literal, Optional, TYPE_CHECKING from bittensor.core.extrinsics.params import RootParams from bittensor.core.types import ExtrinsicResponse @@ -138,3 +138,99 @@ def root_register_extrinsic( except Exception as error: return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + +def set_root_claim_type_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + new_root_claim_type: Literal["Swap", "Keep"], + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> ExtrinsicResponse: + """Sets the root claim type for the coldkey in provided wallet. + + Parameters: + subtensor: Subtensor instance to interact with the blockchain. + wallet: Bittensor Wallet instance. + new_root_claim_type: The new root claim type to set. Could be either "Swap" or "Keep". + 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. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + call = subtensor.compose_call( + call_module="SubtensorModule", + call_function="set_root_claim_type", + call_params=RootParams.set_root_claim_type(new_root_claim_type), + ) + return subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + +def claim_root_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> ExtrinsicResponse: + """Claims the root emissions for a coldkey. + + Parameters: + subtensor: Subtensor instance to interact with the blockchain. + wallet: Bittensor Wallet instance. + 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. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + call = subtensor.compose_call( + call_module="SubtensorModule", + call_function="claim_root", + call_params={}, + ) + return subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) From 2da01f951f66c8e04c4672fcdf5a9fdf30fa0638 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 27 Oct 2025 10:32:48 -0700 Subject: [PATCH 05/21] improve tests framework --- bittensor/extras/dev_framework/subnet.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/bittensor/extras/dev_framework/subnet.py b/bittensor/extras/dev_framework/subnet.py index 7e248ae39e..f0236bea88 100644 --- a/bittensor/extras/dev_framework/subnet.py +++ b/bittensor/extras/dev_framework/subnet.py @@ -8,6 +8,7 @@ sudo_call_extrinsic as async_sudo_call_extrinsic, ) from bittensor.core.extrinsics.utils import sudo_call_extrinsic +from bittensor.core.settings import DEFAULT_PERIOD from bittensor.core.types import ExtrinsicResponse from bittensor.extras import SubtensorApi from bittensor.utils.btlogging import logging @@ -33,7 +34,8 @@ class TestSubnet: def __init__( self, subtensor: SubtensorApi, - period: Optional[int] = None, + netuid: Optional[int] = None, + period: Optional[int] = DEFAULT_PERIOD, raise_error: bool = False, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, @@ -46,10 +48,13 @@ def __init__( self.wait_for_inclusion = wait_for_inclusion self.wait_for_finalization = wait_for_finalization - self._netuid: Optional[int] = None + self._netuid = netuid self._owner: Optional[Wallet] = None self._calls: list[CALL_RECORD] = [] + def __str__(self): + return f"TestSubnet(netuid={self._netuid}, owner={self._owner})" + @property def calls(self) -> list[CALL_RECORD]: return self._calls @@ -270,7 +275,7 @@ def _activate_subnet( "SubtensorModule", "DurationOfStartCall" ).value # added 10 blocks bc 2.5 seconds is not always enough for the chain to update. - self.s.wait_for_block(current_block + activation_block + 10) + self.s.wait_for_block(current_block + activation_block + 1) response = self.s.subnets.start_call( wallet=owner_wallet, netuid=netuid or self._netuid, From 38b77de4ebebc32b3d5db9f2d2fdf53cc7f3f48a Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 27 Oct 2025 10:34:31 -0700 Subject: [PATCH 06/21] add subtensor unit tests --- tests/unit_tests/test_subtensor.py | 48 ++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 976840845d..5cf9b7bfd4 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -4756,3 +4756,51 @@ def test_commit_weights_with_zero_max_attempts( assert isinstance(response.error, ValueError) assert expected_message in str(response.error) assert expected_message in caplog.text + + +def test_get_root_claim_type(mocker, subtensor): + """Tests that `get_root_claim_type` calls proper methods and returns the correct value.""" + # Preps + fake_coldkey_ss58 = mocker.Mock(spec=str) + mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + mocked_map = mocker.patch.object(subtensor.substrate, "query") + + # call + result = subtensor.get_root_claim_type(fake_coldkey_ss58) + + # asserts + mocked_determine_block_hash.assert_called_once() + mocked_map.assert_called_once_with( + module="SubtensorModule", + storage_function="RootClaimType", + params=[fake_coldkey_ss58], + block_hash=mocked_determine_block_hash.return_value, + ) + assert result == mocked_map.return_value + + +def test_set_root_claim_type(mocker, subtensor): + """Tests that `set_root_claim_type` calls proper methods and returns the correct value.""" + # Preps + faked_wallet = mocker.Mock(spec=Wallet) + fake_new_root_claim_type = mocker.Mock(spec=str) + mocked_set_root_claim_type_extrinsic = mocker.patch.object( + subtensor_module, "set_root_claim_type_extrinsic" + ) + + # call + response = subtensor.set_root_claim_type( + wallet=faked_wallet, new_root_claim_type=fake_new_root_claim_type + ) + + # asserts + mocked_set_root_claim_type_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=faked_wallet, + new_root_claim_type=fake_new_root_claim_type, + period=None, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_set_root_claim_type_extrinsic.return_value From 46cbf51f0d36f0f419f7181a49d63adbe159d3cc Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 27 Oct 2025 10:40:16 -0700 Subject: [PATCH 07/21] SubtensorApi --- bittensor/extras/subtensor_api/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bittensor/extras/subtensor_api/utils.py b/bittensor/extras/subtensor_api/utils.py index fce9fd97b1..94bea7eb9a 100644 --- a/bittensor/extras/subtensor_api/utils.py +++ b/bittensor/extras/subtensor_api/utils.py @@ -17,6 +17,7 @@ def add_legacy_methods(subtensor: "SubtensorApi"): subtensor.bonds = subtensor.inner_subtensor.bonds subtensor.burned_register = subtensor.inner_subtensor.burned_register subtensor.chain_endpoint = subtensor.inner_subtensor.chain_endpoint + subtensor.claim_root = subtensor.inner_subtensor.claim_root subtensor.commit_reveal_enabled = subtensor.inner_subtensor.commit_reveal_enabled subtensor.commit_weights = subtensor.inner_subtensor.commit_weights subtensor.contribute_crowdloan = subtensor.inner_subtensor.contribute_crowdloan @@ -196,6 +197,7 @@ def add_legacy_methods(subtensor: "SubtensorApi"): subtensor.set_commitment = subtensor.inner_subtensor.set_commitment subtensor.set_delegate_take = subtensor.inner_subtensor.set_delegate_take subtensor.set_reveal_commitment = subtensor.inner_subtensor.set_reveal_commitment + subtensor.set_root_claim_type = subtensor.inner_subtensor.set_root_claim_type subtensor.set_subnet_identity = subtensor.inner_subtensor.set_subnet_identity subtensor.set_weights = subtensor.inner_subtensor.set_weights subtensor.setup_config = subtensor.inner_subtensor.setup_config From 0db3d277ef46c085afbe3bf84d08adc9b63a54be Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 27 Oct 2025 10:40:28 -0700 Subject: [PATCH 08/21] ruff --- tests/unit_tests/test_subtensor.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 5cf9b7bfd4..24798bd168 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -4763,7 +4763,11 @@ def test_get_root_claim_type(mocker, subtensor): # Preps fake_coldkey_ss58 = mocker.Mock(spec=str) mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") - mocked_map = mocker.patch.object(subtensor.substrate, "query") + fake_type = mocker.Mock(spec=str) + fake_result = {fake_type: ()} + mocked_map = mocker.patch.object( + subtensor.substrate, "query", return_value=fake_result + ) # call result = subtensor.get_root_claim_type(fake_coldkey_ss58) @@ -4776,7 +4780,7 @@ def test_get_root_claim_type(mocker, subtensor): params=[fake_coldkey_ss58], block_hash=mocked_determine_block_hash.return_value, ) - assert result == mocked_map.return_value + assert result == fake_type def test_set_root_claim_type(mocker, subtensor): From e3af49179cb3c6e99274769f9095447ea54857dc Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 27 Oct 2025 10:40:49 -0700 Subject: [PATCH 09/21] add methods in Async/Subtensor --- bittensor/core/async_subtensor.py | 88 ++++++++++++++++++++++++++++++- bittensor/core/subtensor.py | 87 +++++++++++++++++++++++++++++- 2 files changed, 171 insertions(+), 4 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index d0a8c4cfea..e8c5c5b807 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -2,7 +2,7 @@ import copy import ssl from datetime import datetime, timezone -from typing import cast, Optional, Any, Union, Iterable, TYPE_CHECKING +from typing import cast, Optional, Any, Union, Iterable, TYPE_CHECKING, Literal import asyncstdlib as a import scalecodec @@ -73,7 +73,11 @@ register_subnet_extrinsic, set_subnet_identity_extrinsic, ) -from bittensor.core.extrinsics.asyncex.root import root_register_extrinsic +from bittensor.core.extrinsics.asyncex.root import ( + claim_root_extrinsic, + root_register_extrinsic, + set_root_claim_type_extrinsic, +) from bittensor.core.extrinsics.asyncex.serving import ( publish_metadata_extrinsic, ) @@ -3035,6 +3039,21 @@ async def get_revealed_commitment_by_hotkey( return None return tuple(decode_revealed_commitment(pair) for pair in query) + async def get_root_claim_type( + self, + coldkey_ss58: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> dict: + """Retrieves the root claim type for a given coldkey address.""" + return await self.substrate.query( + module="SubtensorModule", + storage_function="RootClaimType", + params=[coldkey_ss58], + block_hash=await self.determine_block_hash(block, block_hash, reuse_block), + ) + async def get_stake( self, coldkey_ss58: str, @@ -5101,6 +5120,37 @@ async def burned_register( wait_for_finalization=wait_for_finalization, ) + async def claim_root( + self, + wallet: "Wallet", + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ): + """Claims the root emissions for a coldkey. + + Parameters: + wallet: Bittensor Wallet instance. + 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. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + return await claim_root_extrinsic( + subtensor=self, + wallet=wallet, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + async def commit_weights( self, wallet: "Wallet", @@ -5973,6 +6023,40 @@ async def set_delegate_take( logging.error(f"[red]{response.message}[/red]") return response + async def set_root_claim_type( + self, + wallet: "Wallet", + new_root_claim_type: Literal["Swap", "Keep"], + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ): + """Sets the root claim type for the coldkey in provided wallet. + + Parameters: + wallet: Bittensor Wallet instance. + new_root_claim_type: The new root claim type to set. Could be either "Swap" or "Keep". + 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. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + return await set_root_claim_type_extrinsic( + subtensor=self, + wallet=wallet, + new_root_claim_type=new_root_claim_type, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + async def set_subnet_identity( self, wallet: "Wallet", diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 2d6bbbdb51..176fa71335 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1,7 +1,7 @@ import copy from datetime import datetime, timezone from functools import lru_cache -from typing import TYPE_CHECKING, Any, Iterable, Optional, Union, cast +from typing import TYPE_CHECKING, Any, Iterable, Optional, Union, cast, Literal import scalecodec from async_substrate_interface.errors import SubstrateRequestException @@ -74,7 +74,11 @@ register_subnet_extrinsic, set_subnet_identity_extrinsic, ) -from bittensor.core.extrinsics.root import root_register_extrinsic +from bittensor.core.extrinsics.root import ( + claim_root_extrinsic, + root_register_extrinsic, + set_root_claim_type_extrinsic, +) from bittensor.core.extrinsics.serving import ( publish_metadata_extrinsic, serve_axon_extrinsic, @@ -2219,6 +2223,20 @@ def get_revealed_commitment_by_hotkey( return None return tuple(decode_revealed_commitment(pair) for pair in query) + def get_root_claim_type( + self, + coldkey_ss58: str, + block: Optional[int] = None, + ) -> str: + """Retrieves the root claim type for a given coldkey address.""" + query = self.substrate.query( + module="SubtensorModule", + storage_function="RootClaimType", + params=[coldkey_ss58], + block_hash=self.determine_block_hash(block), + ) + return next(iter(query.keys())) + def get_stake( self, coldkey_ss58: str, @@ -3883,6 +3901,37 @@ def burned_register( wait_for_finalization=wait_for_finalization, ) + def claim_root( + self, + wallet: "Wallet", + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ): + """Claims the root emissions for a coldkey. + + Parameters: + wallet: Bittensor Wallet instance. + 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. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + return claim_root_extrinsic( + subtensor=self, + wallet=wallet, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + def commit_weights( self, wallet: "Wallet", @@ -4739,6 +4788,40 @@ def set_delegate_take( logging.error(f"[red]{response.message}[/red]") return response + def set_root_claim_type( + self, + wallet: "Wallet", + new_root_claim_type: Literal["Swap", "Keep"], + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ): + """Sets the root claim type for the coldkey in provided wallet. + + Parameters: + wallet: Bittensor Wallet instance. + new_root_claim_type: The new root claim type to set. Could be either "Swap" or "Keep". + 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. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + return set_root_claim_type_extrinsic( + subtensor=self, + wallet=wallet, + new_root_claim_type=new_root_claim_type, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + def set_subnet_identity( self, wallet: "Wallet", From ce132b5facb51cbcdb90e12b1da4ccd2f1db0d01 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 27 Oct 2025 10:43:42 -0700 Subject: [PATCH 10/21] add e2e `test_root_claim.py` --- tests/e2e_tests/test_root_claim.py | 129 +++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 tests/e2e_tests/test_root_claim.py diff --git a/tests/e2e_tests/test_root_claim.py b/tests/e2e_tests/test_root_claim.py new file mode 100644 index 0000000000..1e14417ba9 --- /dev/null +++ b/tests/e2e_tests/test_root_claim.py @@ -0,0 +1,129 @@ +from bittensor.core.extrinsics import root +from bittensor_wallet import Wallet +from bittensor.utils.balance import Balance +from tests.e2e_tests.utils import ( + AdminUtils, + NETUID, + TestSubnet, + ACTIVATE_SUBNET, + REGISTER_SUBNET, + REGISTER_NEURON, + SUDO_SET_ADMIN_FREEZE_WINDOW, + SUDO_SET_TEMPO, +) + + +PROOF_COUNTER = 3 + + +def test_root_claim( + subtensor, alice_wallet, bob_wallet, charlie_wallet, dave_wallet, fred_wallet +): + """Tests root claim logic.""" + TEMPO_TO_SET = 10 if subtensor.chain.is_fast_blocks() else 20 + + # activate ROOT net to stake on Alice + rood_sn = TestSubnet(subtensor, 0) + rood_sn.execute_steps( + [ + SUDO_SET_TEMPO(alice_wallet, AdminUtils, True, NETUID, TEMPO_TO_SET), + ACTIVATE_SUBNET(alice_wallet), + REGISTER_NEURON(alice_wallet), + ] + ) + + sn2 = TestSubnet(subtensor) + sn2.execute_steps( + [ + SUDO_SET_ADMIN_FREEZE_WINDOW(alice_wallet, AdminUtils, True, 0), + REGISTER_SUBNET(bob_wallet), + SUDO_SET_TEMPO(alice_wallet, AdminUtils, True, NETUID, TEMPO_TO_SET), + ACTIVATE_SUBNET(bob_wallet), + REGISTER_NEURON(alice_wallet), + REGISTER_NEURON(charlie_wallet), + ] + ) + + response = subtensor.staking.add_stake_multiple( + wallet=charlie_wallet, + netuids=[sn2.netuid] * 5, + hotkey_ss58s=[alice_wallet.hotkey.ss58_address] * 5, + amounts=[Balance.from_tao(10)] * 5, + ) + assert response.success, response.message + + for k, v in response.data.items(): + print(k, v.data) + + stake_balance = Balance.from_tao(10) + + # stake to Alice in ROOT + response = subtensor.staking.add_stake( + wallet=charlie_wallet, + netuid=rood_sn.netuid, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + amount=stake_balance, + ) + assert response.success, response.message + + # set claim type to Swap (actually it's already Swap, but just to be sure if default is changed in the future) + assert subtensor.staking.set_root_claim_type( + wallet=charlie_wallet, new_root_claim_type="Swap" + ).success + assert ( + subtensor.staking.get_root_claim_type(charlie_wallet.coldkey.ss58_address) + == "Swap" + ) + + proof_counter = PROOF_COUNTER + prev_root_stake = subtensor.staking.get_stake( + coldkey_ss58=charlie_wallet.coldkey.ss58_address, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + netuid=rood_sn.netuid, + ) + # proof that ROOT stake is changing each last epoch block + while proof_counter > 0: + next_epoch_start_block = subtensor.subnets.get_next_epoch_start_block( + rood_sn.netuid + ) + subtensor.wait_for_block(next_epoch_start_block) + charlie_root_stake = subtensor.staking.get_stake( + coldkey_ss58=charlie_wallet.coldkey.ss58_address, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + netuid=rood_sn.netuid, + ) + assert charlie_root_stake > prev_root_stake + proof_counter -= 1 + + # === Set claim type to Keep === + assert subtensor.staking.set_root_claim_type( + wallet=charlie_wallet, new_root_claim_type="Keep" + ).success + assert ( + subtensor.staking.get_root_claim_type(charlie_wallet.coldkey.ss58_address) + == "Keep" + ) + + proof_counter = PROOF_COUNTER + prev_root_stake = subtensor.staking.get_stake( + coldkey_ss58=charlie_wallet.coldkey.ss58_address, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + netuid=rood_sn.netuid, + ) + # proof that ROOT stake isn't changes until it's claimed manually + while proof_counter > 0: + next_epoch_start_block = subtensor.subnets.get_next_epoch_start_block( + rood_sn.netuid + ) + subtensor.wait_for_block(next_epoch_start_block) + charlie_root_stake = subtensor.staking.get_stake( + coldkey_ss58=charlie_wallet.coldkey.ss58_address, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + netuid=rood_sn.netuid, + ) + assert charlie_root_stake == prev_root_stake + proof_counter -= 1 + + # claim ROOT stake + response = subtensor.staking.claim_root(charlie_wallet) + assert response.success, response.message From 29a02dc139a762229cb6ab5ca55f53f96271767c Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Tue, 28 Oct 2025 00:10:21 -0700 Subject: [PATCH 11/21] update dev framework generated helpers --- .../dev_framework/calls/non_sudo_calls.py | 95 ++----------------- .../extras/dev_framework/calls/pallets.py | 5 +- .../extras/dev_framework/calls/sudo_calls.py | 15 ++- 3 files changed, 18 insertions(+), 97 deletions(-) diff --git a/bittensor/extras/dev_framework/calls/non_sudo_calls.py b/bittensor/extras/dev_framework/calls/non_sudo_calls.py index c3aac6e842..c498fe162e 100644 --- a/bittensor/extras/dev_framework/calls/non_sudo_calls.py +++ b/bittensor/extras/dev_framework/calls/non_sudo_calls.py @@ -11,7 +11,7 @@ Note: Any manual changes will be overwritten the next time the generator is run. - Subtensor spec version: 325 + Subtensor spec version: 331 """ from collections import namedtuple @@ -21,12 +21,6 @@ "ADD_LIQUIDITY", ["wallet", "pallet", "hotkey", "netuid", "tick_low", "tick_high", "liquidity"], ) # args: [hotkey: T::AccountId, netuid: NetUid, tick_low: TickIndex, tick_high: TickIndex, liquidity: u64] | Pallet: Swap -ADD_MEMBER = namedtuple( - "ADD_MEMBER", ["wallet", "pallet", "who"] -) # args: [who: AccountIdLookupOf] | Pallet: SenateMembers -ADD_MEMBER = namedtuple( - "ADD_MEMBER", ["wallet", "pallet", "who"] -) # args: [who: AccountIdLookupOf] | Pallet: TriumvirateMembers ADD_PROXY = namedtuple( "ADD_PROXY", ["wallet", "pallet", "delegate", "proxy_type", "delay"] ) # args: [delegate: AccountIdLookupOf, proxy_type: T::ProxyType, delay: BlockNumberFor] | Pallet: Proxy @@ -45,9 +39,6 @@ "allow_partial", ], ) # args: [hotkey: T::AccountId, netuid: NetUid, amount_staked: TaoCurrency, limit_price: TaoCurrency, allow_partial: bool] | Pallet: SubtensorModule -ADJUST_SENATE = namedtuple( - "ADJUST_SENATE", ["wallet", "pallet", "hotkey"] -) # args: [hotkey: T::AccountId] | Pallet: SubtensorModule ANNOUNCE = namedtuple( "ANNOUNCE", ["wallet", "pallet", "real", "call_hash"] ) # args: [real: AccountIdLookupOf, call_hash: CallHashOf] | Pallet: Proxy @@ -160,40 +151,16 @@ CANCEL_RETRY_NAMED = namedtuple( "CANCEL_RETRY_NAMED", ["wallet", "pallet", "id"] ) # args: [id: TaskName] | Pallet: Scheduler -CHANGE_KEY = namedtuple( - "CHANGE_KEY", ["wallet", "pallet", "new"] -) # args: [new: AccountIdLookupOf] | Pallet: SenateMembers -CHANGE_KEY = namedtuple( - "CHANGE_KEY", ["wallet", "pallet", "new"] -) # args: [new: AccountIdLookupOf] | Pallet: TriumvirateMembers -CLEAR_IDENTITY = namedtuple( - "CLEAR_IDENTITY", ["wallet", "pallet", "identified"] -) # args: [identified: T::AccountId] | Pallet: Registry -CLEAR_PRIME = namedtuple( - "CLEAR_PRIME", +CLAIM_ROOT = namedtuple( + "CLAIM_ROOT", [ "wallet", "pallet", ], -) # args: [] | Pallet: SenateMembers -CLEAR_PRIME = namedtuple( - "CLEAR_PRIME", - [ - "wallet", - "pallet", - ], -) # args: [] | Pallet: TriumvirateMembers -CLOSE = namedtuple( - "CLOSE", - [ - "wallet", - "pallet", - "proposal_hash", - "index", - "proposal_weight_bound", - "length_bound", - ], -) # args: [proposal_hash: T::Hash, index: ProposalIndex, proposal_weight_bound: Weight, length_bound: u32] | Pallet: Triumvirate +) # args: [] | Pallet: SubtensorModule +CLEAR_IDENTITY = namedtuple( + "CLEAR_IDENTITY", ["wallet", "pallet", "identified"] +) # args: [identified: T::AccountId] | Pallet: Registry COMMIT_CRV3_MECHANISM_WEIGHTS = namedtuple( "COMMIT_CRV3_MECHANISM_WEIGHTS", ["wallet", "pallet", "netuid", "mecid", "commit", "reveal_round"], @@ -278,9 +245,6 @@ DISABLE_WHITELIST = namedtuple( "DISABLE_WHITELIST", ["wallet", "pallet", "disabled"] ) # args: [disabled: bool] | Pallet: EVM -DISAPPROVE_PROPOSAL = namedtuple( - "DISAPPROVE_PROPOSAL", ["wallet", "pallet", "proposal_hash"] -) # args: [proposal_hash: T::Hash] | Pallet: Triumvirate DISPATCH_AS = namedtuple( "DISPATCH_AS", ["wallet", "pallet", "as_origin", "call"] ) # args: [as_origin: Box, call: Box<::RuntimeCall>] | Pallet: Utility @@ -303,9 +267,6 @@ "pallet", ], ) # args: [] | Pallet: SafeMode -EXECUTE = namedtuple( - "EXECUTE", ["wallet", "pallet", "proposal", "length_bound"] -) # args: [proposal: Box<>::Proposal>, length_bound: u32] | Pallet: Triumvirate EXTEND = namedtuple( "EXTEND", [ @@ -409,9 +370,6 @@ "pallet", ], ) # args: [] | Pallet: Proxy -PROPOSE = namedtuple( - "PROPOSE", ["wallet", "pallet", "proposal", "length_bound", "duration"] -) # args: [proposal: Box<>::Proposal>, length_bound: u32, duration: BlockNumberFor] | Pallet: Triumvirate PROXY = namedtuple( "PROXY", ["wallet", "pallet", "real", "force_proxy_type", "call"] ) # args: [real: AccountIdLookupOf, force_proxy_type: Option, call: Box<::RuntimeCall>] | Pallet: Proxy @@ -472,12 +430,6 @@ REMOVE_LIQUIDITY = namedtuple( "REMOVE_LIQUIDITY", ["wallet", "pallet", "hotkey", "netuid", "position_id"] ) # args: [hotkey: T::AccountId, netuid: NetUid, position_id: PositionId] | Pallet: Swap -REMOVE_MEMBER = namedtuple( - "REMOVE_MEMBER", ["wallet", "pallet", "who"] -) # args: [who: AccountIdLookupOf] | Pallet: SenateMembers -REMOVE_MEMBER = namedtuple( - "REMOVE_MEMBER", ["wallet", "pallet", "who"] -) # args: [who: AccountIdLookupOf] | Pallet: TriumvirateMembers REMOVE_PROXIES = namedtuple( "REMOVE_PROXIES", [ @@ -516,12 +468,6 @@ REQUEST_PREIMAGE = namedtuple( "REQUEST_PREIMAGE", ["wallet", "pallet", "hash"] ) # args: [hash: T::Hash] | Pallet: Preimage -RESET_MEMBERS = namedtuple( - "RESET_MEMBERS", ["wallet", "pallet", "members"] -) # args: [members: Vec] | Pallet: SenateMembers -RESET_MEMBERS = namedtuple( - "RESET_MEMBERS", ["wallet", "pallet", "members"] -) # args: [members: Vec] | Pallet: TriumvirateMembers REVEAL_MECHANISM_WEIGHTS = namedtuple( "REVEAL_MECHANISM_WEIGHTS", ["wallet", "pallet", "netuid", "mecid", "uids", "values", "salt", "version_key"], @@ -656,27 +602,21 @@ "SET_MECHANISM_WEIGHTS", ["wallet", "pallet", "netuid", "mecid", "dests", "weights", "version_key"], ) # args: [netuid: NetUid, mecid: MechId, dests: Vec, weights: Vec, version_key: u64] | Pallet: SubtensorModule -SET_MEMBERS = namedtuple( - "SET_MEMBERS", ["wallet", "pallet", "new_members", "prime", "old_count"] -) # args: [new_members: Vec, prime: Option, old_count: MemberCount] | Pallet: Triumvirate SET_OLDEST_STORED_ROUND = namedtuple( "SET_OLDEST_STORED_ROUND", ["wallet", "pallet", "oldest_round"] ) # args: [oldest_round: u64] | Pallet: Drand SET_PENDING_CHILDKEY_COOLDOWN = namedtuple( "SET_PENDING_CHILDKEY_COOLDOWN", ["wallet", "pallet", "cooldown"] ) # args: [cooldown: u64] | Pallet: SubtensorModule -SET_PRIME = namedtuple( - "SET_PRIME", ["wallet", "pallet", "who"] -) # args: [who: AccountIdLookupOf] | Pallet: SenateMembers -SET_PRIME = namedtuple( - "SET_PRIME", ["wallet", "pallet", "who"] -) # args: [who: AccountIdLookupOf] | Pallet: TriumvirateMembers SET_RETRY = namedtuple( "SET_RETRY", ["wallet", "pallet", "task", "retries", "period"] ) # args: [task: TaskAddress>, retries: u8, period: BlockNumberFor] | Pallet: Scheduler SET_RETRY_NAMED = namedtuple( "SET_RETRY_NAMED", ["wallet", "pallet", "id", "retries", "period"] ) # args: [id: TaskName, retries: u8, period: BlockNumberFor] | Pallet: Scheduler +SET_ROOT_CLAIM_TYPE = namedtuple( + "SET_ROOT_CLAIM_TYPE", ["wallet", "pallet", "new_root_claim_type"] +) # args: [new_root_claim_type: RootClaimTypeEnum] | Pallet: SubtensorModule SET_STORAGE = namedtuple( "SET_STORAGE", ["wallet", "pallet", "items"] ) # args: [items: Vec] | Pallet: System @@ -708,9 +648,6 @@ SUDO = namedtuple( "SUDO", ["wallet", "pallet", "call"] ) # args: [call: Box<::RuntimeCall>] | Pallet: Sudo -SUDO = namedtuple( - "SUDO", ["wallet", "pallet", "call"] -) # args: [call: Box] | Pallet: SubtensorModule SWAP_AUTHORITIES = namedtuple( "SWAP_AUTHORITIES", ["wallet", "pallet", "new_authorities"] ) # args: [new_authorities: BoundedVec<::AuthorityId, T::MaxAuthorities>] | Pallet: AdminUtils @@ -720,12 +657,6 @@ SWAP_HOTKEY = namedtuple( "SWAP_HOTKEY", ["wallet", "pallet", "hotkey", "new_hotkey", "netuid"] ) # args: [hotkey: T::AccountId, new_hotkey: T::AccountId, netuid: Option] | Pallet: SubtensorModule -SWAP_MEMBER = namedtuple( - "SWAP_MEMBER", ["wallet", "pallet", "remove", "add"] -) # args: [remove: AccountIdLookupOf, add: AccountIdLookupOf] | Pallet: SenateMembers -SWAP_MEMBER = namedtuple( - "SWAP_MEMBER", ["wallet", "pallet", "remove", "add"] -) # args: [remove: AccountIdLookupOf, add: AccountIdLookupOf] | Pallet: TriumvirateMembers SWAP_STAKE = namedtuple( "SWAP_STAKE", [ @@ -811,12 +742,6 @@ UPGRADE_ACCOUNTS = namedtuple( "UPGRADE_ACCOUNTS", ["wallet", "pallet", "who"] ) # args: [who: Vec] | Pallet: Balances -VOTE = namedtuple( - "VOTE", ["wallet", "pallet", "hotkey", "proposal", "index", "approve"] -) # args: [hotkey: T::AccountId, proposal: T::Hash, index: u32, approve: bool] | Pallet: SubtensorModule -VOTE = namedtuple( - "VOTE", ["wallet", "pallet", "proposal", "index", "approve"] -) # args: [proposal: T::Hash, index: ProposalIndex, approve: bool] | Pallet: Triumvirate WITHDRAW = namedtuple( "WITHDRAW", ["wallet", "pallet", "address", "value"] ) # args: [address: H160, value: BalanceOf] | Pallet: EVM diff --git a/bittensor/extras/dev_framework/calls/pallets.py b/bittensor/extras/dev_framework/calls/pallets.py index fd49851ee7..b295a3b1ba 100644 --- a/bittensor/extras/dev_framework/calls/pallets.py +++ b/bittensor/extras/dev_framework/calls/pallets.py @@ -1,5 +1,5 @@ """ " -Subtensor spec version: 325 +Subtensor spec version: 331 """ System = "System" @@ -7,9 +7,6 @@ Grandpa = "Grandpa" Balances = "Balances" SubtensorModule = "SubtensorModule" -Triumvirate = "Triumvirate" -TriumvirateMembers = "TriumvirateMembers" -SenateMembers = "SenateMembers" Utility = "Utility" Sudo = "Sudo" Multisig = "Multisig" diff --git a/bittensor/extras/dev_framework/calls/sudo_calls.py b/bittensor/extras/dev_framework/calls/sudo_calls.py index 5702247234..67a25668ae 100644 --- a/bittensor/extras/dev_framework/calls/sudo_calls.py +++ b/bittensor/extras/dev_framework/calls/sudo_calls.py @@ -11,7 +11,7 @@ Note: Any manual changes will be overwritten the next time the generator is run. - Subtensor spec version: 325 + Subtensor spec version: 331 """ from collections import namedtuple @@ -121,10 +121,6 @@ "SUDO_SET_MAX_REGISTRATIONS_PER_BLOCK", ["wallet", "pallet", "sudo", "netuid", "max_registrations_per_block"], ) # args: [netuid: NetUid, max_registrations_per_block: u16] | Pallet: AdminUtils -SUDO_SET_MAX_WEIGHT_LIMIT = namedtuple( - "SUDO_SET_MAX_WEIGHT_LIMIT", - ["wallet", "pallet", "sudo", "netuid", "max_weight_limit"], -) # args: [netuid: NetUid, max_weight_limit: u16] | Pallet: AdminUtils SUDO_SET_MECHANISM_COUNT = namedtuple( "SUDO_SET_MECHANISM_COUNT", ["wallet", "pallet", "sudo", "netuid", "mechanism_count"], @@ -173,6 +169,9 @@ SUDO_SET_NOMINATOR_MIN_REQUIRED_STAKE = namedtuple( "SUDO_SET_NOMINATOR_MIN_REQUIRED_STAKE", ["wallet", "pallet", "sudo", "min_stake"] ) # args: [min_stake: u64] | Pallet: AdminUtils +SUDO_SET_NUM_ROOT_CLAIMS = namedtuple( + "SUDO_SET_NUM_ROOT_CLAIMS", ["wallet", "pallet", "sudo", "new_value"] +) # args: [new_value: u64] | Pallet: SubtensorModule SUDO_SET_OWNER_HPARAM_RATE_LIMIT = namedtuple( "SUDO_SET_OWNER_HPARAM_RATE_LIMIT", ["wallet", "pallet", "sudo", "epochs"] ) # args: [epochs: u16] | Pallet: AdminUtils @@ -190,6 +189,9 @@ SUDO_SET_RHO = namedtuple( "SUDO_SET_RHO", ["wallet", "pallet", "sudo", "netuid", "rho"] ) # args: [netuid: NetUid, rho: u16] | Pallet: AdminUtils +SUDO_SET_ROOT_CLAIM_THRESHOLD = namedtuple( + "SUDO_SET_ROOT_CLAIM_THRESHOLD", ["wallet", "pallet", "sudo", "netuid", "new_value"] +) # args: [netuid: NetUid, new_value: u64] | Pallet: SubtensorModule SUDO_SET_SERVING_RATE_LIMIT = namedtuple( "SUDO_SET_SERVING_RATE_LIMIT", ["wallet", "pallet", "sudo", "netuid", "serving_rate_limit"], @@ -261,6 +263,3 @@ SUDO_UNCHECKED_WEIGHT = namedtuple( "SUDO_UNCHECKED_WEIGHT", ["wallet", "pallet", "sudo", "call", "weight"] ) # args: [call: Box<::RuntimeCall>, weight: Weight] | Pallet: Sudo -SUDO_UNCHECKED_WEIGHT = namedtuple( - "SUDO_UNCHECKED_WEIGHT", ["wallet", "pallet", "sudo", "call", "weight"] -) # args: [call: Box, weight: Weight] | Pallet: SubtensorModule From b8c19c475ab1e18c5d55e7df867a1dda666746a9 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Tue, 28 Oct 2025 00:10:34 -0700 Subject: [PATCH 12/21] update SubtensorApi --- bittensor/extras/subtensor_api/staking.py | 4 ++++ bittensor/extras/subtensor_api/utils.py | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/bittensor/extras/subtensor_api/staking.py b/bittensor/extras/subtensor_api/staking.py index 9e59beada6..2c1c7b15fb 100644 --- a/bittensor/extras/subtensor_api/staking.py +++ b/bittensor/extras/subtensor_api/staking.py @@ -14,6 +14,10 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.get_hotkey_stake = subtensor.get_hotkey_stake self.get_minimum_required_stake = subtensor.get_minimum_required_stake self.get_root_claim_type = subtensor.get_root_claim_type + self.get_root_claimable_all_rates = subtensor.get_root_claimable_all_rates + self.get_root_claimable_rate = subtensor.get_root_claimable_rate + self.get_root_claimable_stake = subtensor.get_root_claimable_stake + self.get_root_claimed = subtensor.get_root_claimed self.get_stake = subtensor.get_stake self.get_stake_add_fee = subtensor.get_stake_add_fee self.get_stake_for_coldkey_and_hotkey = ( diff --git a/bittensor/extras/subtensor_api/utils.py b/bittensor/extras/subtensor_api/utils.py index 94bea7eb9a..37351f6cb5 100644 --- a/bittensor/extras/subtensor_api/utils.py +++ b/bittensor/extras/subtensor_api/utils.py @@ -111,6 +111,16 @@ def add_legacy_methods(subtensor: "SubtensorApi"): subtensor.inner_subtensor.get_revealed_commitment_by_hotkey ) subtensor.get_root_claim_type = subtensor.inner_subtensor.get_root_claim_type + subtensor.get_root_claimable_all_rates = ( + subtensor.inner_subtensor.get_root_claimable_all_rates + ) + subtensor.get_root_claimable_rate = ( + subtensor.inner_subtensor.get_root_claimable_rate + ) + subtensor.get_root_claimable_stake = ( + subtensor.inner_subtensor.get_root_claimable_stake + ) + subtensor.get_root_claimed = subtensor.inner_subtensor.get_root_claimed subtensor.get_stake = subtensor.inner_subtensor.get_stake subtensor.get_stake_add_fee = subtensor.inner_subtensor.get_stake_add_fee subtensor.get_stake_for_coldkey_and_hotkey = ( From c803a46b081b0319e93a11d3bbed2e7ec820fa38 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Tue, 28 Oct 2025 01:45:32 -0700 Subject: [PATCH 13/21] add subtensor methods --- bittensor/core/async_subtensor.py | 168 +++++++++++++++++++++++++++++- bittensor/core/subtensor.py | 131 ++++++++++++++++++++++- 2 files changed, 291 insertions(+), 8 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index e8c5c5b807..d90e00a85f 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -3046,13 +3046,171 @@ async def get_root_claim_type( block_hash: Optional[str] = None, reuse_block: bool = False, ) -> dict: - """Retrieves the root claim type for a given coldkey address.""" - return await self.substrate.query( + """Retrieves the root claim type for a given coldkey address. + + Parameters: + coldkey_ss58: The ss58 address of the coldkey. + block: The block number to query. Do not specify if using block_hash or reuse_block. + block_hash: The block hash at which to check the parameter. Do not set if using block or reuse_block. + reuse_block: Whether to reuse the last-used block hash. Do not set if using block_hash or block. + + Returns: + RootClaimType value in string representation. Could be `Swap` or `Keep`. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + query = await self.substrate.query( module="SubtensorModule", storage_function="RootClaimType", params=[coldkey_ss58], - block_hash=await self.determine_block_hash(block, block_hash, reuse_block), + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + return next(iter(query.keys())) + + async def get_root_claimable_rate( + self, + hotkey_ss58: str, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> float: + """Retrieves the root claimable rate from a given hotkey address for provided netuid. + + Parameters: + hotkey_ss58: The ss58 address of the root validator. + netuid: The unique identifier of the subnet to get the rate. + block: The block number to query. Do not specify if using block_hash or reuse_block. + block_hash: The block hash at which to check the parameter. Do not set if using block or reuse_block. + reuse_block: Whether to reuse the last-used block hash. Do not set if using block_hash or block. + + Returns: + The rate of claimable stake from validator's hotkey ss58 address for provided subnet. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + all_rates = await self.get_root_claimable_all_rates( + hotkey_ss58=hotkey_ss58, + block=block_hash, + ) + return all_rates.get(netuid, 0.0) + + async def get_root_claimable_all_rates( + self, + hotkey_ss58: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> dict[int, float]: + """Retrieves all root claimable rates from a given hotkey address for all subnets with this validator. + + Parameters: + hotkey_ss58: The ss58 address of the root validator. + block: The block number to query. Do not specify if using block_hash or reuse_block. + block_hash: The block hash at which to check the parameter. Do not set if using block or reuse_block. + reuse_block: Whether to reuse the last-used block hash. Do not set if using block_hash or block. + + Returns: + The rate of claimable stake from validator's hotkey ss58 address for provided subnet. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + query = await self.substrate.query( + module="SubtensorModule", + storage_function="RootClaimable", + params=[hotkey_ss58], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + bits_list = next(iter(query.value)) + return {bits[0]: fixed_to_float(bits[1], frac_bits=32) for bits in bits_list} + + async def get_root_claimable_stake( + self, + coldkey_ss58: str, + hotkey_ss58: str, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Balance: + """ + Retrieves the root claimable stake for a given coldkey address. + + Parameters: + coldkey_ss58: Delegate's ColdKey ss58 address. + hotkey_ss58: The root validator hotkey ss58 address. + netuid: Delegate's netuid where stake will be claimed. + block: The block number to query. Do not specify if using block_hash or reuse_block. + block_hash: The block hash at which to check the parameter. Do not set if using block or reuse_block. + reuse_block: Whether to reuse the last-used block hash. Do not set if using block_hash or block. + + Returns: + Available for claiming root stake. + + Note: + After manual claim, claimable (available) stake will be added to subtends stake. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + root_stake = await self.get_stake( + coldkey_ss58=coldkey_ss58, + hotkey_ss58=hotkey_ss58, + netuid=0, # root netuid + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + root_claimable_rate = await self.get_root_claimable_rate( + hotkey_ss58=hotkey_ss58, + netuid=netuid, + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + root_claimable_stake = (root_claimable_rate * root_stake).set_unit( + netuid=netuid + ) + root_claimed = await self.get_root_claimed( + coldkey_ss58=coldkey_ss58, + hotkey_ss58=hotkey_ss58, + netuid=netuid, + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + return max( + root_claimable_stake - root_claimed, Balance(0).set_unit(netuid=netuid) + ) + + async def get_root_claimed( + self, + coldkey_ss58: str, + hotkey_ss58: str, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Balance: + """Retrieves the root claimed Alpha shares for coldkey from hotkey in provided subnet. + + Parameters: + coldkey_ss58: The ss58 address of the staker. + hotkey_ss58: The ss58 address of the root validator. + netuid: The unique identifier of the subnet. + block: The block number to query. Do not specify if using block_hash or reuse_block. + block_hash: The block hash at which to check the parameter. Do not set if using block or reuse_block. + reuse_block: Whether to reuse the last-used block hash. Do not set if using block_hash or block. + + Returns: + The number of Alpha stake claimed from the root validator in Rao. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + query = await self.substrate.query( + module="SubtensorModule", + storage_function="RootClaimed", + params=[hotkey_ss58, coldkey_ss58, netuid], + block_hash=block_hash, + reuse_block_hash=reuse_block, ) + return Balance.from_rao(query.value).set_unit(netuid=netuid) async def get_stake( self, @@ -5123,7 +5281,7 @@ async def burned_register( async def claim_root( self, wallet: "Wallet", - period: Optional[int] = None, + period: Optional[int] = DEFAULT_PERIOD, raise_error: bool = False, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, @@ -6027,7 +6185,7 @@ async def set_root_claim_type( self, wallet: "Wallet", new_root_claim_type: Literal["Swap", "Keep"], - period: Optional[int] = None, + period: Optional[int] = DEFAULT_PERIOD, raise_error: bool = False, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 176fa71335..c8aa38fed6 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -2228,7 +2228,15 @@ def get_root_claim_type( coldkey_ss58: str, block: Optional[int] = None, ) -> str: - """Retrieves the root claim type for a given coldkey address.""" + """Retrieves the root claim type for a given coldkey address. + + Parameters: + coldkey_ss58: The ss58 address of the coldkey. + block: The block number to query. + + Returns: + RootClaimType value in string representation. Could be `Swap` or `Keep`. + """ query = self.substrate.query( module="SubtensorModule", storage_function="RootClaimType", @@ -2237,6 +2245,123 @@ def get_root_claim_type( ) return next(iter(query.keys())) + def get_root_claimable_rate( + self, + hotkey_ss58: str, + netuid: int, + block: Optional[int] = None, + ) -> float: + """Retrieves the root claimable rate from a given hotkey address for provided netuid. + + Parameters: + hotkey_ss58: The ss58 address of the root validator. + netuid: The unique identifier of the subnet to get the rate. + block: The blockchain block number for the query. + + Returns: + The rate of claimable stake from validator's hotkey ss58 address for provided subnet. + """ + all_rates = self.get_root_claimable_all_rates( + hotkey_ss58=hotkey_ss58, + block=block, + ) + return all_rates.get(netuid, 0.0) + + def get_root_claimable_all_rates( + self, + hotkey_ss58: str, + block: Optional[int] = None, + ) -> dict[int, float]: + """Retrieves all root claimable rates from a given hotkey address for all subnets with this validator. + + Parameters: + hotkey_ss58: The ss58 address of the root validator. + block: The blockchain block number for the query. + + Returns: + The rate of claimable stake from validator's hotkey ss58 address for provided subnet. + """ + query = self.substrate.query( + module="SubtensorModule", + storage_function="RootClaimable", + params=[hotkey_ss58], + block_hash=self.determine_block_hash(block), + ) + bits_list = next(iter(query.value)) + return {bits[0]: fixed_to_float(bits[1], frac_bits=32) for bits in bits_list} + + def get_root_claimable_stake( + self, + coldkey_ss58: str, + hotkey_ss58: str, + netuid: int, + block: Optional[int] = None, + ) -> Balance: + """ + Retrieves the root claimable stake for a given coldkey address. + + Parameters: + coldkey_ss58: Delegate's ColdKey ss58 address. + hotkey_ss58: The root validator hotkey ss58 address. + netuid: Delegate's netuid where stake will be claimed. + block: The blockchain block number for the query. + + Returns: + Available for claiming root stake. + + Note: + After manual claim, claimable (available) stake will be added to subtends stake. + """ + root_stake = self.get_stake( + coldkey_ss58=coldkey_ss58, + hotkey_ss58=hotkey_ss58, + netuid=0, # root netuid + block=block, + ) + root_claimable_rate = self.get_root_claimable_rate( + hotkey_ss58=hotkey_ss58, + netuid=netuid, + block=block, + ) + root_claimable_stake = (root_claimable_rate * root_stake).set_unit( + netuid=netuid + ) + root_claimed = self.get_root_claimed( + coldkey_ss58=coldkey_ss58, + hotkey_ss58=hotkey_ss58, + block=block, + netuid=netuid, + ) + return max( + root_claimable_stake - root_claimed, Balance(0).set_unit(netuid=netuid) + ) + + def get_root_claimed( + self, + coldkey_ss58: str, + hotkey_ss58: str, + netuid: int, + block: Optional[int] = None, + ) -> Balance: + """Retrieves the root claimed Alpha shares for coldkey from hotkey in provided subnet. + + Parameters: + coldkey_ss58: The ss58 address of the staker. + hotkey_ss58: The ss58 address of the root validator. + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + + Returns: + The number of Alpha stake claimed from the root validator in Rao. + """ + query = self.substrate.query( + module="SubtensorModule", + storage_function="RootClaimed", + params=[hotkey_ss58, coldkey_ss58, netuid], + block_hash=self.determine_block_hash(block), + ) + return Balance.from_rao(query.value).set_unit(netuid=netuid) + def get_stake( self, coldkey_ss58: str, @@ -3904,7 +4029,7 @@ def burned_register( def claim_root( self, wallet: "Wallet", - period: Optional[int] = None, + period: Optional[int] = DEFAULT_PERIOD, raise_error: bool = False, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, @@ -4792,7 +4917,7 @@ def set_root_claim_type( self, wallet: "Wallet", new_root_claim_type: Literal["Swap", "Keep"], - period: Optional[int] = None, + period: Optional[int] = DEFAULT_PERIOD, raise_error: bool = False, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, From 070bd342229d963ea7b9c14da2646bb44ef9ad81 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Tue, 28 Oct 2025 01:45:45 -0700 Subject: [PATCH 14/21] add unit tests --- .../extrinsics/asyncex/test_root.py | 70 ++++++ tests/unit_tests/extrinsics/test_root.py | 66 ++++++ tests/unit_tests/test_async_subtensor.py | 221 ++++++++++++++++++ tests/unit_tests/test_subtensor.py | 159 ++++++++++++- 4 files changed, 515 insertions(+), 1 deletion(-) diff --git a/tests/unit_tests/extrinsics/asyncex/test_root.py b/tests/unit_tests/extrinsics/asyncex/test_root.py index aa3b4a0de8..a8c88a64c1 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_root.py +++ b/tests/unit_tests/extrinsics/asyncex/test_root.py @@ -331,3 +331,73 @@ async def test_root_register_extrinsic_uid_not_found(subtensor, fake_wallet, moc reuse_block_hash=False, ) assert result.success is False + + +@pytest.mark.asyncio +async def test_set_root_claim_type_extrinsic(subtensor, fake_wallet, mocker): + """Tests `set_root_claim_type_extrinsic` extrinsic function.""" + # Preps + new_root_claim_type = mocker.Mock(spec=int) + mocked_root_params = mocker.patch.object( + async_root.RootParams, "set_root_claim_type" + ) + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # call + response = await async_root.set_root_claim_type_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + new_root_claim_type=new_root_claim_type, + ) + + # asserts + mocked_root_params.assert_called_once_with(new_root_claim_type) + mocked_compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="set_root_claim_type", + call_params=mocked_root_params.return_value, + ) + mocked_sign_and_send_extrinsic.assert_awaited_once_with( + call=mocked_compose_call.return_value, + wallet=fake_wallet, + period=None, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_sign_and_send_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_claim_root_extrinsic(subtensor, fake_wallet, mocker): + """Tests `claim_root_extrinsic` extrinsic function.""" + # Preps + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # call + response = await async_root.claim_root_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + ) + + # asserts + mocked_compose_call.assert_awaited_once_with( + call_module="SubtensorModule", + call_function="claim_root", + call_params={}, + ) + mocked_sign_and_send_extrinsic.assert_awaited_once_with( + call=mocked_compose_call.return_value, + wallet=fake_wallet, + period=None, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_sign_and_send_extrinsic.return_value diff --git a/tests/unit_tests/extrinsics/test_root.py b/tests/unit_tests/extrinsics/test_root.py index bd40eae28a..be0f1d2532 100644 --- a/tests/unit_tests/extrinsics/test_root.py +++ b/tests/unit_tests/extrinsics/test_root.py @@ -139,3 +139,69 @@ def test_root_register_extrinsic_insufficient_balance( block=mock_subtensor.get_current_block.return_value, ) mock_subtensor.substrate.submit_extrinsic.assert_not_called() + + +def test_set_root_claim_type_extrinsic(subtensor, fake_wallet, mocker): + """Tests `set_root_claim_type_extrinsic` extrinsic function.""" + # Preps + new_root_claim_type = mocker.Mock(spec=int) + mocked_root_params = mocker.patch.object(root.RootParams, "set_root_claim_type") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # call + response = root.set_root_claim_type_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + new_root_claim_type=new_root_claim_type, + ) + + # asserts + mocked_root_params.assert_called_once_with(new_root_claim_type) + mocked_compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="set_root_claim_type", + call_params=mocked_root_params.return_value, + ) + mocked_sign_and_send_extrinsic.assert_called_once_with( + call=mocked_compose_call.return_value, + wallet=fake_wallet, + period=None, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_sign_and_send_extrinsic.return_value + + +def test_claim_root_extrinsic(subtensor, fake_wallet, mocker): + """Tests `claim_root_extrinsic` extrinsic function.""" + # Preps + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # call + response = root.claim_root_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + ) + + # asserts + mocked_compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="claim_root", + call_params={}, + ) + mocked_sign_and_send_extrinsic.assert_called_once_with( + call=mocked_compose_call.return_value, + wallet=fake_wallet, + period=None, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_sign_and_send_extrinsic.return_value diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index f1677df4ee..707d7cb5f7 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -4661,3 +4661,224 @@ async def test_commit_weights_with_zero_max_attempts( assert isinstance(response.error, ValueError) assert expected_message in str(response.error) assert expected_message in caplog.text + + +@pytest.mark.asyncio +async def test_get_root_claim_type(mocker, subtensor): + """Tests that `get_root_claim_type` calls proper methods and returns the correct value.""" + # Preps + fake_coldkey_ss58 = mocker.Mock(spec=str) + mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + fake_type = mocker.Mock(spec=str) + fake_result = {fake_type: ()} + mocked_map = mocker.patch.object( + subtensor.substrate, "query", return_value=fake_result + ) + + # call + result = await subtensor.get_root_claim_type(fake_coldkey_ss58) + + # asserts + mocked_determine_block_hash.assert_awaited_once() + mocked_map.assert_awaited_once_with( + module="SubtensorModule", + storage_function="RootClaimType", + params=[fake_coldkey_ss58], + block_hash=mocked_determine_block_hash.return_value, + reuse_block_hash=False, + ) + assert result == fake_type + + +@pytest.mark.asyncio +async def test_get_root_claimable_rate(mocker, subtensor): + """Tests `get_root_claimable_rate` method.""" + # Preps + hotkey_ss58 = mocker.Mock(spec=str) + netuid = mocker.Mock(spec=int) + + mocked_get_root_claimable_all_rates = mocker.patch.object( + subtensor, "get_root_claimable_all_rates", return_value={} + ) + + # Call + result = await subtensor.get_root_claimable_rate( + hotkey_ss58=hotkey_ss58, + netuid=netuid, + ) + + # Asserts + mocked_get_root_claimable_all_rates.assert_awaited_once_with( + hotkey_ss58=hotkey_ss58, + block=None, + ) + assert result == 0.0 + + +@pytest.mark.asyncio +async def test_get_root_claimable_all_rates(mocker, subtensor): + """Tests `get_root_claimable_all_rates` method.""" + # Preps + hotkey_ss58 = mocker.Mock(spec=str) + mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + fake_value = [((14, {"bits": 6520190}),)] + fake_result = mocker.MagicMock(value=fake_value) + fake_result.__iter__ = fake_value + mocked_query = mocker.patch.object( + subtensor.substrate, "query", return_value=fake_result + ) + mocked_fixed_to_float = mocker.patch.object(async_subtensor, "fixed_to_float") + + # Call + result = await subtensor.get_root_claimable_all_rates( + hotkey_ss58=hotkey_ss58, + ) + + # Asserts + mocked_determine_block_hash.assert_awaited_once() + mocked_query.assert_awaited_once_with( + module="SubtensorModule", + storage_function="RootClaimable", + params=[hotkey_ss58], + block_hash=mocked_determine_block_hash.return_value, + reuse_block_hash=False, + ) + mocked_fixed_to_float.assert_called_once_with({"bits": 6520190}, frac_bits=32) + assert result == {14: mocked_fixed_to_float.return_value} + + +@pytest.mark.asyncio +async def test_get_root_claimable_stake(mocker, subtensor): + """Tests `get_root_claimable_stake` method.""" + # Preps + coldkey_ss58 = mocker.Mock(spec=str) + hotkey_ss58 = mocker.Mock(spec=str) + netuid = 14 + + fake_result = mocker.AsyncMock(return_value=Balance.from_tao(1)) + mocked_get_stake = mocker.patch.object( + subtensor, "get_stake", return_value=fake_result + ) + mocked_get_root_claimable_rate = mocker.patch.object( + subtensor, "get_root_claimable_rate", return_value=0.5 + ) + mocked_get_root_claimed = mocker.patch.object(subtensor, "get_root_claimed") + + # Call + result = await subtensor.get_root_claimable_stake( + coldkey_ss58=coldkey_ss58, + hotkey_ss58=hotkey_ss58, + netuid=netuid, + ) + + # Asserts + mocked_get_stake.assert_awaited_once_with( + coldkey_ss58=coldkey_ss58, + hotkey_ss58=hotkey_ss58, + netuid=0, + block=None, + block_hash=None, + reuse_block=False, + ) + mocked_get_root_claimable_rate.assert_awaited_once_with( + hotkey_ss58=hotkey_ss58, + netuid=netuid, + block=None, + block_hash=None, + reuse_block=False, + ) + mocked_get_root_claimed.assert_awaited_once_with( + coldkey_ss58=coldkey_ss58, + hotkey_ss58=hotkey_ss58, + netuid=netuid, + block=None, + block_hash=None, + reuse_block=False, + ) + assert result == Balance.from_rao(1).set_unit(netuid) + + +@pytest.mark.asyncio +async def test_get_root_claimed(mocker, subtensor): + """Tests `get_root_claimed` method.""" + # Preps + coldkey_ss58 = mocker.Mock(spec=str) + hotkey_ss58 = mocker.Mock(spec=str) + netuid = 14 + fake_value = mocker.Mock(value=1) + mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + mocked_query = mocker.patch.object( + subtensor.substrate, "query", return_value=fake_value + ) + + # Call + result = await subtensor.get_root_claimed( + coldkey_ss58=coldkey_ss58, + hotkey_ss58=hotkey_ss58, + netuid=netuid, + ) + + # Asserts + mocked_determine_block_hash.assert_awaited_once() + mocked_query.assert_awaited_once_with( + module="SubtensorModule", + storage_function="RootClaimed", + params=[hotkey_ss58, coldkey_ss58, netuid], + block_hash=mocked_determine_block_hash.return_value, + reuse_block_hash=False, + ) + assert result == Balance.from_rao(1).set_unit(netuid) + + +@pytest.mark.asyncio +async def test_claim_root(mocker, subtensor): + """Tests `claim_root` extrinsic call method.""" + # preps + wallet = mocker.Mock(spec=Wallet) + mocked_claim_root_extrinsic = mocker.patch.object( + async_subtensor, "claim_root_extrinsic" + ) + + # call + response = await subtensor.claim_root( + wallet=wallet, + ) + + # asserts + mocked_claim_root_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + wallet=wallet, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_claim_root_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_set_root_claim_type(mocker, subtensor): + """Tests that `set_root_claim_type` calls proper methods and returns the correct value.""" + # Preps + faked_wallet = mocker.Mock(spec=Wallet) + fake_new_root_claim_type = mocker.Mock(spec=str) + mocked_set_root_claim_type_extrinsic = mocker.patch.object( + async_subtensor, "set_root_claim_type_extrinsic" + ) + + # call + response = await subtensor.set_root_claim_type( + wallet=faked_wallet, new_root_claim_type=fake_new_root_claim_type + ) + + # asserts + mocked_set_root_claim_type_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + wallet=faked_wallet, + new_root_claim_type=fake_new_root_claim_type, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_set_root_claim_type_extrinsic.return_value diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 24798bd168..edfb5ad63e 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -4783,6 +4783,163 @@ def test_get_root_claim_type(mocker, subtensor): assert result == fake_type +def test_get_root_claimable_rate(mocker, subtensor): + """Tests `get_root_claimable_rate` method.""" + # Preps + hotkey_ss58 = mocker.Mock(spec=str) + netuid = mocker.Mock(spec=int) + + mocked_get_root_claimable_all_rates = mocker.patch.object( + subtensor, "get_root_claimable_all_rates" + ) + + # Call + result = subtensor.get_root_claimable_rate( + hotkey_ss58=hotkey_ss58, + netuid=netuid, + ) + + # Asserts + mocked_get_root_claimable_all_rates.assert_called_once_with( + hotkey_ss58=hotkey_ss58, + block=None, + ) + mocked_get_root_claimable_all_rates.return_value.get.assert_called_once_with( + netuid, 0.0 + ) + assert result == mocked_get_root_claimable_all_rates.return_value.get.return_value + + +def test_get_root_claimable_all_rates(mocker, subtensor): + """Tests `get_root_claimable_all_rates` method.""" + # Preps + hotkey_ss58 = mocker.Mock(spec=str) + mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + fake_value = [((14, {"bits": 6520190}),)] + fake_result = mocker.MagicMock(value=fake_value) + fake_result.__iter__ = fake_value + mocked_query = mocker.patch.object( + subtensor.substrate, "query", return_value=fake_result + ) + mocked_fixed_to_float = mocker.patch.object(subtensor_module, "fixed_to_float") + + # Call + result = subtensor.get_root_claimable_all_rates( + hotkey_ss58=hotkey_ss58, + ) + + # Asserts + mocked_determine_block_hash.assert_called_once() + mocked_query.assert_called_once_with( + module="SubtensorModule", + storage_function="RootClaimable", + params=[hotkey_ss58], + block_hash=mocked_determine_block_hash.return_value, + ) + mocked_fixed_to_float.assert_called_once_with({"bits": 6520190}, frac_bits=32) + assert result == {14: mocked_fixed_to_float.return_value} + + +def test_get_root_claimable_stake(mocker, subtensor): + """Tests `get_root_claimable_stake` method.""" + # Preps + coldkey_ss58 = mocker.Mock(spec=str) + hotkey_ss58 = mocker.Mock(spec=str) + netuid = 14 + + mocked_get_stake = mocker.patch.object( + subtensor, "get_stake", return_value=Balance.from_tao(1) + ) + mocked_get_root_claimable_rate = mocker.patch.object( + subtensor, "get_root_claimable_rate", return_value=0.5 + ) + mocked_get_root_claimed = mocker.patch.object( + subtensor, "get_root_claimed", spec=int + ) + + # Call + result = subtensor.get_root_claimable_stake( + coldkey_ss58=coldkey_ss58, + hotkey_ss58=hotkey_ss58, + netuid=netuid, + ) + + # Asserts + mocked_get_stake.assert_called_once_with( + coldkey_ss58=coldkey_ss58, + hotkey_ss58=hotkey_ss58, + netuid=0, + block=None, + ) + mocked_get_root_claimable_rate.assert_called_once_with( + hotkey_ss58=hotkey_ss58, + netuid=netuid, + block=None, + ) + mocked_get_root_claimed.assert_called_once_with( + coldkey_ss58=coldkey_ss58, + hotkey_ss58=hotkey_ss58, + block=None, + netuid=netuid, + ) + assert result == Balance.from_rao(1).set_unit(netuid) + + +def test_get_root_claimed(mocker, subtensor): + """Tests `get_root_claimed` method.""" + # Preps + coldkey_ss58 = mocker.Mock(spec=str) + hotkey_ss58 = mocker.Mock(spec=str) + netuid = 14 + fake_value = mocker.Mock(value=1) + mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + mocked_query = mocker.patch.object( + subtensor.substrate, "query", return_value=fake_value + ) + + # Call + result = subtensor.get_root_claimed( + coldkey_ss58=coldkey_ss58, + hotkey_ss58=hotkey_ss58, + netuid=netuid, + ) + + # Asserts + mocked_determine_block_hash.assert_called_once() + mocked_query.assert_called_once_with( + module="SubtensorModule", + storage_function="RootClaimed", + params=[hotkey_ss58, coldkey_ss58, netuid], + block_hash=mocked_determine_block_hash.return_value, + ) + assert result == Balance.from_rao(1).set_unit(netuid) + + +def test_claim_root(mocker, subtensor): + """Tests `claim_root` extrinsic call method.""" + # preps + wallet = mocker.Mock(spec=Wallet) + mocked_claim_root_extrinsic = mocker.patch.object( + subtensor_module, "claim_root_extrinsic" + ) + + # call + response = subtensor.claim_root( + wallet=wallet, + ) + + # asserts + mocked_claim_root_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=wallet, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_claim_root_extrinsic.return_value + + def test_set_root_claim_type(mocker, subtensor): """Tests that `set_root_claim_type` calls proper methods and returns the correct value.""" # Preps @@ -4802,7 +4959,7 @@ def test_set_root_claim_type(mocker, subtensor): subtensor=subtensor, wallet=faked_wallet, new_root_claim_type=fake_new_root_claim_type, - period=None, + period=DEFAULT_PERIOD, raise_error=False, wait_for_inclusion=True, wait_for_finalization=True, From 13f242a9663c5b872adcd04b7ee3c54da71964a2 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Tue, 28 Oct 2025 01:46:02 -0700 Subject: [PATCH 15/21] add e2e tests --- tests/e2e_tests/test_root_claim.py | 348 +++++++++++++++++++++++++---- 1 file changed, 306 insertions(+), 42 deletions(-) diff --git a/tests/e2e_tests/test_root_claim.py b/tests/e2e_tests/test_root_claim.py index 1e14417ba9..5be0992f2d 100644 --- a/tests/e2e_tests/test_root_claim.py +++ b/tests/e2e_tests/test_root_claim.py @@ -1,6 +1,5 @@ -from bittensor.core.extrinsics import root -from bittensor_wallet import Wallet from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging from tests.e2e_tests.utils import ( AdminUtils, NETUID, @@ -9,29 +8,37 @@ REGISTER_SUBNET, REGISTER_NEURON, SUDO_SET_ADMIN_FREEZE_WINDOW, + SUDO_SET_NUM_ROOT_CLAIMS, SUDO_SET_TEMPO, ) +PROOF_COUNTER = 2 -PROOF_COUNTER = 3 +def test_root_claim_swap(subtensor, alice_wallet, bob_wallet, charlie_wallet): + """Tests root claim Swap logic. -def test_root_claim( - subtensor, alice_wallet, bob_wallet, charlie_wallet, dave_wallet, fred_wallet -): - """Tests root claim logic.""" + Steps: + - activate ROOT net to stake on Alice + - Register SN and the same validator (Alice) on that subnet to ROOT has an emissions + - Make sure CK has claim type as Swap + - Stake to Alice in ROOT + - Checks in the loop with PROOF_COUNTER numbers of epochs that stake in increased in normative (auto) way + """ TEMPO_TO_SET = 10 if subtensor.chain.is_fast_blocks() else 20 - # activate ROOT net to stake on Alice - rood_sn = TestSubnet(subtensor, 0) - rood_sn.execute_steps( + # Activate ROOT net to stake on Alice + root_sn = TestSubnet(subtensor, 0) + root_sn.execute_steps( [ SUDO_SET_TEMPO(alice_wallet, AdminUtils, True, NETUID, TEMPO_TO_SET), ACTIVATE_SUBNET(alice_wallet), + # Until owner HK isn't Alice HK, we need to register Alice to stake on its HK REGISTER_NEURON(alice_wallet), ] ) + # Register SN and the same validator (Alice) on that subnet to ROOT has an emissions sn2 = TestSubnet(subtensor) sn2.execute_steps( [ @@ -40,90 +47,347 @@ def test_root_claim( SUDO_SET_TEMPO(alice_wallet, AdminUtils, True, NETUID, TEMPO_TO_SET), ACTIVATE_SUBNET(bob_wallet), REGISTER_NEURON(alice_wallet), - REGISTER_NEURON(charlie_wallet), ] ) - response = subtensor.staking.add_stake_multiple( - wallet=charlie_wallet, - netuids=[sn2.netuid] * 5, - hotkey_ss58s=[alice_wallet.hotkey.ss58_address] * 5, - amounts=[Balance.from_tao(10)] * 5, - ) - assert response.success, response.message - - for k, v in response.data.items(): - print(k, v.data) - stake_balance = Balance.from_tao(10) - # stake to Alice in ROOT + # Stake to Alice in ROOT response = subtensor.staking.add_stake( wallet=charlie_wallet, - netuid=rood_sn.netuid, + netuid=root_sn.netuid, hotkey_ss58=alice_wallet.hotkey.ss58_address, amount=stake_balance, ) assert response.success, response.message - # set claim type to Swap (actually it's already Swap, but just to be sure if default is changed in the future) + # Make sure stake is the same or greater (if auto claim happened) as before + stake_info_before = subtensor.staking.get_stake_for_coldkey_and_hotkey( + coldkey_ss58=charlie_wallet.coldkey.ss58_address, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + netuids=[root_sn.netuid], + ) + assert stake_info_before[root_sn.netuid].stake >= stake_balance + + # Set claim type to Swap (actually it's already Swap, but just to be sure if default is changed in the future) assert subtensor.staking.set_root_claim_type( wallet=charlie_wallet, new_root_claim_type="Swap" ).success assert ( - subtensor.staking.get_root_claim_type(charlie_wallet.coldkey.ss58_address) + subtensor.staking.get_root_claim_type( + coldkey_ss58=charlie_wallet.coldkey.ss58_address + ) == "Swap" ) + # We skip the era in which the stake was installed, since the emission doesn't occur (Subtensor implementation) + logging.console.info(f"Skipping stake epoch") + next_epoch_start_block = subtensor.subnets.get_next_epoch_start_block( + netuid=root_sn.netuid + ) + subtensor.wait_for_block(block=next_epoch_start_block) + + # We do the check over a few epochs proof_counter = PROOF_COUNTER prev_root_stake = subtensor.staking.get_stake( coldkey_ss58=charlie_wallet.coldkey.ss58_address, hotkey_ss58=alice_wallet.hotkey.ss58_address, - netuid=rood_sn.netuid, + netuid=root_sn.netuid, ) - # proof that ROOT stake is changing each last epoch block + + # Proof that ROOT stake is changing each last epoch block while proof_counter > 0: next_epoch_start_block = subtensor.subnets.get_next_epoch_start_block( - rood_sn.netuid + netuid=root_sn.netuid ) - subtensor.wait_for_block(next_epoch_start_block) + subtensor.wait_for_block(block=next_epoch_start_block) charlie_root_stake = subtensor.staking.get_stake( coldkey_ss58=charlie_wallet.coldkey.ss58_address, hotkey_ss58=alice_wallet.hotkey.ss58_address, - netuid=rood_sn.netuid, + netuid=root_sn.netuid, ) assert charlie_root_stake > prev_root_stake + prev_root_stake = charlie_root_stake proof_counter -= 1 - # === Set claim type to Keep === + +def test_root_claim_keep_with_zero_num_root_auto_claims( + subtensor, alice_wallet, bob_wallet, charlie_wallet +): + """Tests root claim Keep logic when NumRootClaim is 0, RootClaimType is Keep. + + Steps: + - Disable admin freeze window + - Set NumRootClaim to 0 (THE DEVIL IS IN THE DETAILS, logic compliantly broken without this bc of random auto claim) + - Activate ROOT net to stake on Alice + - Set claim type to Keep to staker CK + - Register SN2 and the same validator (Alice) on that subnet to ROOT has an emissions + - Make NumRootClaim is 0 to avoid auto claims from random list of coldkeys (just fot test) + - Make sure CK has claim type as Keep + - Stake to Alice in ROOT + - Checks in the loop with PROOF_COUNTER numbers of epochs and check that claimed is 0 and stake is not changed. + - Root claim manually + - Check that claimed is recalculated and stake is increased for delegate + """ + TEMPO_TO_SET = 50 if subtensor.chain.is_fast_blocks() else 10 + + # Activate ROOT net to stake on Alice + # To important set NumRootClaim to 0, so that we can check that it's not changed with random auto claim. + # Random auto claim is happening even if CK has root claim type as `Keep` + root_sn = TestSubnet(subtensor, 0) + root_sn.execute_steps( + [ + SUDO_SET_ADMIN_FREEZE_WINDOW(alice_wallet, AdminUtils, True, 0), + SUDO_SET_NUM_ROOT_CLAIMS(alice_wallet, "SubtensorModule", True, 0), + SUDO_SET_TEMPO(alice_wallet, AdminUtils, True, NETUID, TEMPO_TO_SET), + ACTIVATE_SUBNET(alice_wallet), + REGISTER_NEURON(alice_wallet), + ] + ) + + # Set claim type to Keep assert subtensor.staking.set_root_claim_type( wallet=charlie_wallet, new_root_claim_type="Keep" ).success + + # Register SN2 and the same validator (Alice) on that subnet to ROOT has an emissions + # - owner - Bob + # - validator - Alice + # - neuron, staker - Charlie (doesn't stake to validator in SN2, in ROOT only) + sn2 = TestSubnet(subtensor) + sn2.execute_steps( + [ + SUDO_SET_ADMIN_FREEZE_WINDOW(alice_wallet, AdminUtils, True, 0), + REGISTER_SUBNET(bob_wallet), + SUDO_SET_TEMPO(alice_wallet, AdminUtils, True, NETUID, TEMPO_TO_SET), + ACTIVATE_SUBNET(bob_wallet), + REGISTER_NEURON(alice_wallet), + REGISTER_NEURON(charlie_wallet), + ] + ) + + # Make NumRootClaim is 0 (imposable to test if not 0) + assert subtensor.queries.query_subtensor("NumRootClaim").value == 0 + + # Make sure CK has claim type as Keep assert ( - subtensor.staking.get_root_claim_type(charlie_wallet.coldkey.ss58_address) + subtensor.staking.get_root_claim_type( + coldkey_ss58=charlie_wallet.coldkey.ss58_address + ) == "Keep" ) - proof_counter = PROOF_COUNTER - prev_root_stake = subtensor.staking.get_stake( - coldkey_ss58=charlie_wallet.coldkey.ss58_address, + stake_balance = Balance.from_tao(1000) # just a dream - stake 1000 TAO to SN0 :D + + # Stake from Charlie to Alice in ROOT + response = subtensor.staking.add_stake( + wallet=charlie_wallet, + netuid=root_sn.netuid, hotkey_ss58=alice_wallet.hotkey.ss58_address, - netuid=rood_sn.netuid, + amount=stake_balance, ) + assert response.success, response.message + + proof_counter = PROOF_COUNTER + # proof that ROOT stake isn't changes until it's claimed manually while proof_counter > 0: next_epoch_start_block = subtensor.subnets.get_next_epoch_start_block( - rood_sn.netuid + root_sn.netuid ) subtensor.wait_for_block(next_epoch_start_block) - charlie_root_stake = subtensor.staking.get_stake( + + # Check Charlie stake and claimed + claimed_stake_charlie = subtensor.staking.get_stake( + coldkey_ss58=charlie_wallet.coldkey.ss58_address, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + netuid=sn2.netuid, + ) + assert claimed_stake_charlie == 0 + + root_claimed_charlie = subtensor.staking.get_root_claimed( coldkey_ss58=charlie_wallet.coldkey.ss58_address, hotkey_ss58=alice_wallet.hotkey.ss58_address, - netuid=rood_sn.netuid, + netuid=sn2.netuid, ) - assert charlie_root_stake == prev_root_stake + assert root_claimed_charlie == 0 + proof_counter -= 1 - # claim ROOT stake + # === Check Charlie before manual claim === + claimed_before_charlie = subtensor.staking.get_root_claimed( + coldkey_ss58=charlie_wallet.coldkey.ss58_address, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + netuid=sn2.netuid, + ) + assert claimed_before_charlie == 0 + + claimable_stake_before_charlie = subtensor.staking.get_root_claimable_stake( + coldkey_ss58=charlie_wallet.coldkey.ss58_address, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + netuid=sn2.netuid, + ) + assert claimable_stake_before_charlie != 0 + + stake_before_charlie = subtensor.staking.get_stake( + coldkey_ss58=charlie_wallet.coldkey.ss58_address, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + netuid=sn2.netuid, + ) + assert stake_before_charlie == 0 + + logging.console.info(f"[blue]Charlie before:[/blue]") + logging.console.info(f"RootClaimed: {claimed_before_charlie}") + logging.console.info(f"RootClaimable stake: {claimable_stake_before_charlie}") + logging.console.info(f"SN2 Stake: {stake_before_charlie}") + + # === ROOT CLAIM MANUAL === response = subtensor.staking.claim_root(charlie_wallet) assert response.success, response.message + block = subtensor.block + + # === Check Charlie after manual claim === + claimed_after_charlie = subtensor.staking.get_root_claimed( + coldkey_ss58=charlie_wallet.coldkey.ss58_address, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + netuid=sn2.netuid, + block=block, + ) + assert claimed_after_charlie >= claimable_stake_before_charlie + + claimable_stake_after_charlie = subtensor.staking.get_root_claimable_stake( + coldkey_ss58=charlie_wallet.coldkey.ss58_address, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + netuid=sn2.netuid, + block=block, + ) + assert claimable_stake_after_charlie == claimed_before_charlie + + stake_after_charlie = subtensor.staking.get_stake( + coldkey_ss58=charlie_wallet.coldkey.ss58_address, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + netuid=sn2.netuid, + block=block, + ) + assert stake_after_charlie >= claimable_stake_before_charlie + + logging.console.info(f"[blue]Charlie after:[/blue]") + logging.console.info(f"RootClaimed: {claimed_after_charlie}") + logging.console.info(f"RootClaimable stake: {claimable_stake_after_charlie}") + logging.console.info(f"SN2 Stake: {stake_after_charlie}") + + +def test_root_claim_keep_with_random_auto_claims( + subtensor, alice_wallet, bob_wallet, charlie_wallet, dave_wallet +): + """Tests root claim Keep logic when NumRootClaim is greater than 0, RootClaimType is Keep. + + Steps: + - Disable admin freeze window + - Activate ROOT net to stake on Alice + - Set claim type to Keep to staker CK + - Register SN2 and the same validator (Alice) on that subnet to ROOT has an emissions + - Check NumRootClaim is 5 to have random auto claims logic + - Make sure CK has claim type as Keep + - Stake to Alice in ROOT + - Checks in the loop with PROOF_COUNTER numbers of epochs and check that claimed is greater than the previous one + and stake is increased for delegate + """ + TEMPO_TO_SET = 20 if subtensor.chain.is_fast_blocks() else 10 + + # Activate ROOT net to stake on Alice + # To important set NumRootClaim to 0, so that we can check that it's not changed with random auto claim. + # Random auto claim is happening even if CK has root claim type as `Keep` + root_sn = TestSubnet(subtensor, 0) + root_sn.execute_steps( + [ + SUDO_SET_ADMIN_FREEZE_WINDOW(alice_wallet, AdminUtils, True, 0), + SUDO_SET_TEMPO(alice_wallet, AdminUtils, True, NETUID, TEMPO_TO_SET), + ACTIVATE_SUBNET(alice_wallet), + REGISTER_NEURON(alice_wallet), + ] + ) + + # Set claim type to Keep + assert subtensor.staking.set_root_claim_type( + wallet=charlie_wallet, new_root_claim_type="Keep" + ).success + + # Register SN2 and the same validator (Alice) on that subnet to ROOT has an emissions + # - owner - Bob + # - validator - Alice + # - neuron, staker - Charlie (doesn't stake to validator in SN2, in ROOT only) + sn2 = TestSubnet(subtensor) + sn2.execute_steps( + [ + SUDO_SET_ADMIN_FREEZE_WINDOW(alice_wallet, AdminUtils, True, 0), + REGISTER_SUBNET(bob_wallet), + SUDO_SET_TEMPO(alice_wallet, AdminUtils, True, NETUID, TEMPO_TO_SET), + ACTIVATE_SUBNET(bob_wallet), + REGISTER_NEURON(alice_wallet), + REGISTER_NEURON(charlie_wallet), + ] + ) + + # Make NumRootClaim is 5 (to have random auto claims logic) + assert subtensor.queries.query_subtensor("NumRootClaim").value == 5 + + # Make sure CK has claim type as Keep + assert ( + subtensor.staking.get_root_claim_type( + coldkey_ss58=charlie_wallet.coldkey.ss58_address + ) + == "Keep" + ) + + stake_balance = Balance.from_tao(1000) # just a dream - stake 1000 TAO to SN0 :D + + # Stake from Charlie to Alice in ROOT + response = subtensor.staking.add_stake( + wallet=charlie_wallet, + netuid=root_sn.netuid, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + amount=stake_balance, + ) + assert response.success, response.message + + proof_counter = PROOF_COUNTER + + prev_claimed_stake_charlie = subtensor.staking.get_stake( + coldkey_ss58=charlie_wallet.coldkey.ss58_address, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + netuid=sn2.netuid, + ) + + prev_root_claimed_charlie = subtensor.staking.get_root_claimed( + coldkey_ss58=charlie_wallet.coldkey.ss58_address, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + netuid=sn2.netuid, + ) + + # proof that ROOT stake increases each epoch even RootClaimType is Keep bc of random auto claim takes min 5 coldkeys + # to do release emissions + while proof_counter > 0: + next_epoch_start_block = subtensor.subnets.get_next_epoch_start_block( + root_sn.netuid + ) + subtensor.wait_for_block(next_epoch_start_block) + + # Check Charlie stake and claimed + claimed_stake_charlie = subtensor.staking.get_stake( + coldkey_ss58=charlie_wallet.coldkey.ss58_address, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + netuid=sn2.netuid, + ) + assert claimed_stake_charlie > prev_claimed_stake_charlie + prev_claimed_stake_charlie = claimed_stake_charlie + + root_claimed_charlie = subtensor.staking.get_root_claimed( + coldkey_ss58=charlie_wallet.coldkey.ss58_address, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + netuid=sn2.netuid, + ) + assert root_claimed_charlie > prev_root_claimed_charlie + prev_root_claimed_charlie = root_claimed_charlie + + proof_counter -= 1 From 13f72720cdde19c7d9db3095c2240db84703cfd7 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Tue, 28 Oct 2025 17:17:54 -0700 Subject: [PATCH 16/21] update extrinsics with netuids --- bittensor/core/extrinsics/asyncex/root.py | 5 ++++- bittensor/core/extrinsics/params/root.py | 9 +++++++++ bittensor/core/extrinsics/root.py | 6 ++++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/bittensor/core/extrinsics/asyncex/root.py b/bittensor/core/extrinsics/asyncex/root.py index 58427eb157..e999fb506e 100644 --- a/bittensor/core/extrinsics/asyncex/root.py +++ b/bittensor/core/extrinsics/asyncex/root.py @@ -10,6 +10,7 @@ if TYPE_CHECKING: from bittensor_wallet import Wallet from bittensor.core.async_subtensor import AsyncSubtensor + from bittensor.core.types import UIDs async def _get_limits(subtensor: "AsyncSubtensor") -> tuple[int, float]: @@ -196,6 +197,7 @@ async def set_root_claim_type_extrinsic( async def claim_root_extrinsic( subtensor: "AsyncSubtensor", wallet: "Wallet", + netuids: "UIDs", period: Optional[int] = None, raise_error: bool = False, wait_for_inclusion: bool = True, @@ -206,6 +208,7 @@ async def claim_root_extrinsic( Parameters: subtensor: Subtensor instance to interact with the blockchain. wallet: Bittensor Wallet instance. + netuids: The netuids to claim root emissions for. 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. @@ -225,7 +228,7 @@ async def claim_root_extrinsic( call = await subtensor.compose_call( call_module="SubtensorModule", call_function="claim_root", - call_params={}, + call_params=RootParams.claim_root(netuids), ) return await subtensor.sign_and_send_extrinsic( call=call, diff --git a/bittensor/core/extrinsics/params/root.py b/bittensor/core/extrinsics/params/root.py index f547c8a5d3..7b62a3a652 100644 --- a/bittensor/core/extrinsics/params/root.py +++ b/bittensor/core/extrinsics/params/root.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from bittensor.core.types import UIDs @dataclass @@ -18,3 +19,11 @@ def set_root_claim_type( ) -> dict: """Returns the parameters for the `set_root_claim_type`.""" return {"new_root_claim_type": new_root_claim_type} + + @classmethod + def claim_root( + cls, + netuids: UIDs, + ) -> dict: + """Returns the parameters for the `claim_root`.""" + return {"subnets": netuids} diff --git a/bittensor/core/extrinsics/root.py b/bittensor/core/extrinsics/root.py index 29a4539f9f..b556aecff2 100644 --- a/bittensor/core/extrinsics/root.py +++ b/bittensor/core/extrinsics/root.py @@ -2,7 +2,7 @@ from typing import Literal, Optional, TYPE_CHECKING from bittensor.core.extrinsics.params import RootParams -from bittensor.core.types import ExtrinsicResponse +from bittensor.core.types import ExtrinsicResponse, UIDs from bittensor.utils import u16_normalized_float from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging @@ -192,6 +192,7 @@ def set_root_claim_type_extrinsic( def claim_root_extrinsic( subtensor: "Subtensor", wallet: "Wallet", + netuids: "UIDs", period: Optional[int] = None, raise_error: bool = False, wait_for_inclusion: bool = True, @@ -202,6 +203,7 @@ def claim_root_extrinsic( Parameters: subtensor: Subtensor instance to interact with the blockchain. wallet: Bittensor Wallet instance. + netuids: The netuids to claim root emissions for. 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. @@ -221,7 +223,7 @@ def claim_root_extrinsic( call = subtensor.compose_call( call_module="SubtensorModule", call_function="claim_root", - call_params={}, + call_params=RootParams.claim_root(netuids), ) return subtensor.sign_and_send_extrinsic( call=call, From 7479ab21185ca25cd29eeaa1e9eb319cce530dd9 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Tue, 28 Oct 2025 17:18:04 -0700 Subject: [PATCH 17/21] update subtensors --- bittensor/core/async_subtensor.py | 3 +++ bittensor/core/subtensor.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index d90e00a85f..dab649e892 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -5281,6 +5281,7 @@ async def burned_register( async def claim_root( self, wallet: "Wallet", + netuids: "UIDs", period: Optional[int] = DEFAULT_PERIOD, raise_error: bool = False, wait_for_inclusion: bool = True, @@ -5290,6 +5291,7 @@ async def claim_root( Parameters: wallet: Bittensor Wallet instance. + netuids: The netuids to claim root emissions for. 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. @@ -5303,6 +5305,7 @@ async def claim_root( return await claim_root_extrinsic( subtensor=self, wallet=wallet, + netuids=netuids, period=period, raise_error=raise_error, wait_for_inclusion=wait_for_inclusion, diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index c8aa38fed6..f8e4b7deec 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -4029,6 +4029,7 @@ def burned_register( def claim_root( self, wallet: "Wallet", + netuids: "UIDs", period: Optional[int] = DEFAULT_PERIOD, raise_error: bool = False, wait_for_inclusion: bool = True, @@ -4038,6 +4039,7 @@ def claim_root( Parameters: wallet: Bittensor Wallet instance. + netuids: The netuids to claim root emissions for. 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. @@ -4051,6 +4053,7 @@ def claim_root( return claim_root_extrinsic( subtensor=self, wallet=wallet, + netuids=netuids, period=period, raise_error=raise_error, wait_for_inclusion=wait_for_inclusion, From 274d0ed626dfbcba5354fb78d0d226e8aa5987e4 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Tue, 28 Oct 2025 17:18:16 -0700 Subject: [PATCH 18/21] update unit tests --- tests/unit_tests/extrinsics/asyncex/test_root.py | 6 +++++- tests/unit_tests/extrinsics/test_root.py | 6 +++++- tests/unit_tests/test_async_subtensor.py | 3 +++ tests/unit_tests/test_subtensor.py | 3 +++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/unit_tests/extrinsics/asyncex/test_root.py b/tests/unit_tests/extrinsics/asyncex/test_root.py index a8c88a64c1..ddb755b671 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_root.py +++ b/tests/unit_tests/extrinsics/asyncex/test_root.py @@ -375,6 +375,8 @@ async def test_set_root_claim_type_extrinsic(subtensor, fake_wallet, mocker): async def test_claim_root_extrinsic(subtensor, fake_wallet, mocker): """Tests `claim_root_extrinsic` extrinsic function.""" # Preps + netuids = mocker.Mock(spec=list) + mocked_root_params = mocker.patch.object(async_root.RootParams, "claim_root") mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic" @@ -384,13 +386,15 @@ async def test_claim_root_extrinsic(subtensor, fake_wallet, mocker): response = await async_root.claim_root_extrinsic( subtensor=subtensor, wallet=fake_wallet, + netuids=netuids, ) # asserts + mocked_root_params.assert_called_once_with(netuids) mocked_compose_call.assert_awaited_once_with( call_module="SubtensorModule", call_function="claim_root", - call_params={}, + call_params=mocked_root_params.return_value, ) mocked_sign_and_send_extrinsic.assert_awaited_once_with( call=mocked_compose_call.return_value, diff --git a/tests/unit_tests/extrinsics/test_root.py b/tests/unit_tests/extrinsics/test_root.py index be0f1d2532..068ed9fc9a 100644 --- a/tests/unit_tests/extrinsics/test_root.py +++ b/tests/unit_tests/extrinsics/test_root.py @@ -179,6 +179,8 @@ def test_set_root_claim_type_extrinsic(subtensor, fake_wallet, mocker): def test_claim_root_extrinsic(subtensor, fake_wallet, mocker): """Tests `claim_root_extrinsic` extrinsic function.""" # Preps + netuids = mocker.Mock(spec=list) + mocked_root_params = mocker.patch.object(root.RootParams, "claim_root") mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic" @@ -188,13 +190,15 @@ def test_claim_root_extrinsic(subtensor, fake_wallet, mocker): response = root.claim_root_extrinsic( subtensor=subtensor, wallet=fake_wallet, + netuids=netuids, ) # asserts + mocked_root_params.assert_called_once_with(netuids) mocked_compose_call.assert_called_once_with( call_module="SubtensorModule", call_function="claim_root", - call_params={}, + call_params=mocked_root_params.return_value, ) mocked_sign_and_send_extrinsic.assert_called_once_with( call=mocked_compose_call.return_value, diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 707d7cb5f7..4914d01c1f 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -4835,6 +4835,7 @@ async def test_claim_root(mocker, subtensor): """Tests `claim_root` extrinsic call method.""" # preps wallet = mocker.Mock(spec=Wallet) + netuids = mocker.Mock(spec=int) mocked_claim_root_extrinsic = mocker.patch.object( async_subtensor, "claim_root_extrinsic" ) @@ -4842,12 +4843,14 @@ async def test_claim_root(mocker, subtensor): # call response = await subtensor.claim_root( wallet=wallet, + netuids=netuids, ) # asserts mocked_claim_root_extrinsic.assert_awaited_once_with( subtensor=subtensor, wallet=wallet, + netuids=netuids, period=DEFAULT_PERIOD, raise_error=False, wait_for_inclusion=True, diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index edfb5ad63e..b06cc269da 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -4919,6 +4919,7 @@ def test_claim_root(mocker, subtensor): """Tests `claim_root` extrinsic call method.""" # preps wallet = mocker.Mock(spec=Wallet) + netuids = mocker.Mock(spec=list) mocked_claim_root_extrinsic = mocker.patch.object( subtensor_module, "claim_root_extrinsic" ) @@ -4926,12 +4927,14 @@ def test_claim_root(mocker, subtensor): # call response = subtensor.claim_root( wallet=wallet, + netuids=netuids, ) # asserts mocked_claim_root_extrinsic.assert_called_once_with( subtensor=subtensor, wallet=wallet, + netuids=netuids, period=DEFAULT_PERIOD, raise_error=False, wait_for_inclusion=True, From a57988876d9098adbe0289d7de521a831baf57b6 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Tue, 28 Oct 2025 17:18:28 -0700 Subject: [PATCH 19/21] improve e2e test --- tests/e2e_tests/test_root_claim.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/e2e_tests/test_root_claim.py b/tests/e2e_tests/test_root_claim.py index 5be0992f2d..b49ff271c4 100644 --- a/tests/e2e_tests/test_root_claim.py +++ b/tests/e2e_tests/test_root_claim.py @@ -33,8 +33,6 @@ def test_root_claim_swap(subtensor, alice_wallet, bob_wallet, charlie_wallet): [ SUDO_SET_TEMPO(alice_wallet, AdminUtils, True, NETUID, TEMPO_TO_SET), ACTIVATE_SUBNET(alice_wallet), - # Until owner HK isn't Alice HK, we need to register Alice to stake on its HK - REGISTER_NEURON(alice_wallet), ] ) @@ -141,7 +139,6 @@ def test_root_claim_keep_with_zero_num_root_auto_claims( SUDO_SET_NUM_ROOT_CLAIMS(alice_wallet, "SubtensorModule", True, 0), SUDO_SET_TEMPO(alice_wallet, AdminUtils, True, NETUID, TEMPO_TO_SET), ACTIVATE_SUBNET(alice_wallet), - REGISTER_NEURON(alice_wallet), ] ) @@ -242,7 +239,7 @@ def test_root_claim_keep_with_zero_num_root_auto_claims( logging.console.info(f"SN2 Stake: {stake_before_charlie}") # === ROOT CLAIM MANUAL === - response = subtensor.staking.claim_root(charlie_wallet) + response = subtensor.staking.claim_root(wallet=charlie_wallet, netuids=[sn2.netuid]) assert response.success, response.message block = subtensor.block @@ -304,7 +301,6 @@ def test_root_claim_keep_with_random_auto_claims( SUDO_SET_ADMIN_FREEZE_WINDOW(alice_wallet, AdminUtils, True, 0), SUDO_SET_TEMPO(alice_wallet, AdminUtils, True, NETUID, TEMPO_TO_SET), ACTIVATE_SUBNET(alice_wallet), - REGISTER_NEURON(alice_wallet), ] ) From b0f2d31c1592e6d962aa4fd36bf2e22eb174da32 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Wed, 29 Oct 2025 17:44:25 -0700 Subject: [PATCH 20/21] test_blocks_async --- tests/e2e_tests/test_subtensor_functions.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/e2e_tests/test_subtensor_functions.py b/tests/e2e_tests/test_subtensor_functions.py index e7160074fb..529d13524c 100644 --- a/tests/e2e_tests/test_subtensor_functions.py +++ b/tests/e2e_tests/test_subtensor_functions.py @@ -393,21 +393,21 @@ def test_blocks(subtensor): @pytest.mark.asyncio -async def test_blocks_async(subtensor): +async def test_blocks_async(async_subtensor): """ Async tests: - Get current block - Get block hash - Wait for block """ - block = subtensor.chain.get_current_block() - assert block == subtensor.block + block = await async_subtensor.chain.get_current_block() + assert block == await async_subtensor.block - block_hash = subtensor.chain.get_block_hash(block) + block_hash = await async_subtensor.chain.get_block_hash(block) assert re.match("0x[a-z0-9]{64}", block_hash) - subtensor.wait_for_block(block + 10) - assert subtensor.chain.get_current_block() in [block + 10, block + 11] + await async_subtensor.wait_for_block(block + 10) + assert await async_subtensor.chain.get_current_block() in [block + 10, block + 11] logging.console.info("✅ Passed [blue]test_blocks_async[/blue]") From fbfb0e0f84265491226ce3482e2875d21ffe098d Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Fri, 31 Oct 2025 13:13:33 -0500 Subject: [PATCH 21/21] update return type --- 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 dab649e892..9d44c45766 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -3045,7 +3045,7 @@ async def get_root_claim_type( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> dict: + ) -> str: """Retrieves the root claim type for a given coldkey address. Parameters: