diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index eaf6dcf4f6..9d44c45766 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, ) @@ -2424,6 +2428,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 ) @@ -2447,25 +2462,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]) @@ -3029,6 +3039,179 @@ 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, + ) -> str: + """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=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, coldkey_ss58: str, @@ -5095,6 +5278,40 @@ async def burned_register( wait_for_finalization=wait_for_finalization, ) + async def claim_root( + self, + wallet: "Wallet", + netuids: "UIDs", + period: Optional[int] = DEFAULT_PERIOD, + 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. + 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. + 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, + netuids=netuids, + 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", @@ -5967,6 +6184,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] = DEFAULT_PERIOD, + 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/extrinsics/asyncex/root.py b/bittensor/core/extrinsics/asyncex/root.py index 45ea91878e..e999fb506e 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 @@ -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]: @@ -142,3 +143,101 @@ 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", + netuids: "UIDs", + 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. + 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. + 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=RootParams.claim_root(netuids), + ) + 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..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 @@ -10,3 +11,19 @@ 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} + + @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 b924a60018..b556aecff2 100644 --- a/bittensor/core/extrinsics/root.py +++ b/bittensor/core/extrinsics/root.py @@ -1,8 +1,8 @@ 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 +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 @@ -138,3 +138,101 @@ 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", + netuids: "UIDs", + 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. + 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. + 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=RootParams.claim_root(netuids), + ) + 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) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 19aefdea24..f8e4b7deec 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, @@ -1700,6 +1704,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 @@ -1723,7 +1737,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, @@ -1737,13 +1751,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: @@ -2216,6 +2223,145 @@ 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. + + 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", + params=[coldkey_ss58], + block_hash=self.determine_block_hash(block), + ) + 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, @@ -3880,6 +4026,40 @@ def burned_register( wait_for_finalization=wait_for_finalization, ) + def claim_root( + self, + wallet: "Wallet", + netuids: "UIDs", + period: Optional[int] = DEFAULT_PERIOD, + 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. + 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. + 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, + netuids=netuids, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + def commit_weights( self, wallet: "Wallet", @@ -4736,6 +4916,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] = DEFAULT_PERIOD, + 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", 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 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, 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..2c1c7b15fb 100644 --- a/bittensor/extras/subtensor_api/staking.py +++ b/bittensor/extras/subtensor_api/staking.py @@ -9,9 +9,15 @@ 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_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 = ( @@ -23,6 +29,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..37351f6cb5 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 @@ -109,6 +110,17 @@ 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_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 = ( @@ -195,6 +207,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 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/e2e_tests/test_root_claim.py b/tests/e2e_tests/test_root_claim.py new file mode 100644 index 0000000000..b49ff271c4 --- /dev/null +++ b/tests/e2e_tests/test_root_claim.py @@ -0,0 +1,389 @@ +from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging +from tests.e2e_tests.utils import ( + AdminUtils, + NETUID, + TestSubnet, + ACTIVATE_SUBNET, + REGISTER_SUBNET, + REGISTER_NEURON, + SUDO_SET_ADMIN_FREEZE_WINDOW, + SUDO_SET_NUM_ROOT_CLAIMS, + SUDO_SET_TEMPO, +) + +PROOF_COUNTER = 2 + + +def test_root_claim_swap(subtensor, alice_wallet, bob_wallet, charlie_wallet): + """Tests root claim Swap 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 + root_sn = TestSubnet(subtensor, 0) + root_sn.execute_steps( + [ + SUDO_SET_TEMPO(alice_wallet, AdminUtils, True, NETUID, TEMPO_TO_SET), + ACTIVATE_SUBNET(alice_wallet), + ] + ) + + # Register SN and the same validator (Alice) on that subnet to ROOT has an emissions + 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), + ] + ) + + stake_balance = Balance.from_tao(10) + + # Stake 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 + + # 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( + 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=root_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( + netuid=root_sn.netuid + ) + 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=root_sn.netuid, + ) + assert charlie_root_stake > prev_root_stake + prev_root_stake = charlie_root_stake + proof_counter -= 1 + + +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), + ] + ) + + # 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( + 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 + + # 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( + 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 == 0 + + 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 == 0 + + proof_counter -= 1 + + # === 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(wallet=charlie_wallet, netuids=[sn2.netuid]) + 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), + ] + ) + + # 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 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]") diff --git a/tests/unit_tests/extrinsics/asyncex/test_root.py b/tests/unit_tests/extrinsics/asyncex/test_root.py index aa3b4a0de8..ddb755b671 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_root.py +++ b/tests/unit_tests/extrinsics/asyncex/test_root.py @@ -331,3 +331,77 @@ 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 + 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" + ) + + # call + 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=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 diff --git a/tests/unit_tests/extrinsics/test_root.py b/tests/unit_tests/extrinsics/test_root.py index bd40eae28a..068ed9fc9a 100644 --- a/tests/unit_tests/extrinsics/test_root.py +++ b/tests/unit_tests/extrinsics/test_root.py @@ -139,3 +139,73 @@ 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 + 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" + ) + + # call + 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=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 diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index ca5bb121ed..4914d01c1f 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -3494,7 +3494,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) @@ -3514,8 +3514,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]) @@ -4659,3 +4661,227 @@ 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) + netuids = mocker.Mock(spec=int) + mocked_claim_root_extrinsic = mocker.patch.object( + async_subtensor, "claim_root_extrinsic" + ) + + # 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, + 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 04b1bc1f51..b06cc269da 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) @@ -4753,3 +4756,215 @@ 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") + 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) + + # 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 == 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) + netuids = mocker.Mock(spec=list) + mocked_claim_root_extrinsic = mocker.patch.object( + subtensor_module, "claim_root_extrinsic" + ) + + # 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, + 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 + 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=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_set_root_claim_type_extrinsic.return_value