diff --git a/.circleci/config.yml b/.circleci/config.yml index eb524eda71..c610dd7b6c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -38,8 +38,8 @@ jobs: command: | python -m venv .venv . .venv/bin/activate - python -m pip install --upgrade pip - pip install ruff -c requirements/dev.txt + python -m pip install --upgrade uv + uv pip install ruff -c requirements/dev.txt - save_cache: name: Save cached ruff venv @@ -98,20 +98,20 @@ jobs: command: | python -m venv .venv . .venv/bin/activate - python -m pip install --upgrade pip - python -m pip install '.[dev]' + python -m pip install --upgrade uv + uv sync --all-extras --dev - save_cache: name: Save cached venv paths: - - "env/" + - "venv/" key: v2-pypi-py<< parameters.python-version >>-{{ checksum "requirements/prod.txt" }}+{{ checksum "requirements/dev.txt" }} - run: name: Install Bittensor command: | . .venv/bin/activate - pip install -e '.[dev]' + uv sync --all-extras --dev - run: name: Instantiate Mock Wallet @@ -189,9 +189,9 @@ jobs: command: | python -m venv .venv . .venv/bin/activate - python -m pip install --upgrade pip - python -m pip install '.[dev]' - pip install flake8 + python -m pip install --upgrade uv + uv sync --all-extras --dev + uv pip install flake8 - save_cache: name: Save cached venv @@ -203,7 +203,7 @@ jobs: name: Install Bittensor command: | . .venv/bin/activate - pip install -e '.[dev]' + uv sync --all-extras --dev - run: name: Lint with flake8 @@ -232,7 +232,7 @@ jobs: - run: name: Combine Coverage command: | - pip3 install --upgrade coveralls + uv pip install --upgrade coveralls coveralls --finish --rcfile .coveragerc || echo "Failed to upload coverage" check-version-updated: diff --git a/.github/workflows/e2e-subtensor-tests.yaml b/.github/workflows/e2e-subtensor-tests.yaml index a32971a918..a95bece4bc 100644 --- a/.github/workflows/e2e-subtensor-tests.yaml +++ b/.github/workflows/e2e-subtensor-tests.yaml @@ -6,10 +6,10 @@ concurrency: on: push: - branches: [main, development, staging] + branches: [master, development, staging] pull_request: - branches: [main, development, staging] + branches: [master, development, staging] types: [ opened, synchronize, reopened, ready_for_review ] workflow_dispatch: diff --git a/CHANGELOG.md b/CHANGELOG.md index e28ba24627..d0c88fa7c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## 9.1.0 /2025-03-12 + +## What's Changed +* Refactor duplicated unittests code by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2724 +* Use uv for circleci by @thewhaleking in https://github.com/opentensor/bittensor/pull/2729 +* Fix E2E test_metagraph_info by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2728 +* Tests: deduplicate fake_wallet and correctly create Mock by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2730 +* E2E Test: wait cooldown period to check set_children effect by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2733 +* Tests: wait for Miner/Validator to fully start by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2737 +* All metagraph subtensor methods now use block by @thewhaleking in https://github.com/opentensor/bittensor/pull/2738 +* Tests: increse test_incentive timeout + fix sudo_set_weights_set_rate_limit by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2739 +* Feat/safe staking by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2736 +* 9.0.5: Bumps version and changelog by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2741 +* Tests: enable E2E test_batch_operations by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2744 +* Fix: burned_register supports root subnet (netuid=0 param) by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2732 +* Feat: set_delegate_take by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2731 +* Renames rate_threshold -> rate_tolerance by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2745 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.0.4...v9.1.0 + ## 9.0.4 /2025-03-06 ## What's Changed diff --git a/VERSION b/VERSION index 93c8cbd8ab..e977f5eae6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -9.0.4 \ No newline at end of file +9.1.0 \ No newline at end of file diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 02cc910f2a..546be4a9d0 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -31,7 +31,7 @@ from bittensor.core.chain_data.delegate_info import DelegatedInfo from bittensor.core.chain_data.utils import decode_metadata from bittensor.core.config import Config -from bittensor.core.errors import SubstrateRequestException +from bittensor.core.errors import ChainError, SubstrateRequestException from bittensor.core.extrinsics.asyncex.commit_reveal import commit_reveal_v3_extrinsic from bittensor.core.extrinsics.asyncex.registration import ( burned_register_extrinsic, @@ -57,6 +57,10 @@ add_stake_extrinsic, add_stake_multiple_extrinsic, ) +from bittensor.core.extrinsics.asyncex.take import ( + decrease_take_extrinsic, + increase_take_extrinsic, +) from bittensor.core.extrinsics.asyncex.transfer import transfer_extrinsic from bittensor.core.extrinsics.asyncex.unstaking import ( unstake_extrinsic, @@ -1111,7 +1115,7 @@ async def get_delegate_take( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> Optional[float]: + ) -> float: """ Retrieves the delegate 'take' percentage for a neuron identified by its hotkey. The 'take' represents the percentage of rewards that the delegate claims from its nominators' stakes. @@ -1123,7 +1127,7 @@ async def get_delegate_take( reuse_block (bool): Whether to reuse the last-used block hash. Returns: - Optional[float]: The delegate take percentage, None if not available. + float: The delegate take percentage. The delegate take is a critical parameter in the network's incentive structure, influencing the distribution of rewards among neurons and their nominators. @@ -1135,11 +1139,8 @@ async def get_delegate_take( reuse_block=reuse_block, params=[hotkey_ss58], ) - return ( - None - if result is None - else u16_normalized_float(getattr(result, "value", 0)) - ) + + return u16_normalized_float(result.value) # type: ignore async def get_delegated( self, @@ -2748,6 +2749,7 @@ async def sign_and_send_extrinsic( use_nonce: bool = False, period: Optional[int] = None, nonce_key: str = "hotkey", + raise_error: bool = False, ) -> tuple[bool, str]: """ Helper method to sign and submit an extrinsic call to chain. @@ -2758,6 +2760,7 @@ async def sign_and_send_extrinsic( wait_for_inclusion (bool): whether to wait until the extrinsic call is included on the chain wait_for_finalization (bool): whether to wait until the extrinsic call is finalized on the chain sign_with: the wallet's keypair to use for the signing. Options are "coldkey", "hotkey", "coldkeypub" + raise_error: raises relevant exception rather than returning `False` if unsuccessful. Returns: (success, error message) @@ -2795,9 +2798,15 @@ async def sign_and_send_extrinsic( if await response.is_success: return True, "" + if raise_error: + raise ChainError.from_error(response.error_message) + return False, format_error_message(await response.error_message) except SubstrateRequestException as e: + if raise_error: + raise + return False, format_error_message(e) # Extrinsics ======================================================================================================= @@ -2810,6 +2819,9 @@ async def add_stake( amount: Optional[Balance] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + safe_staking: bool = False, + allow_partial_stake: bool = False, + rate_tolerance: float = 0.005, ) -> bool: """ Adds the specified amount of stake to a neuron identified by the hotkey ``SS58`` address. @@ -2823,12 +2835,20 @@ async def add_stake( amount (Balance): The amount of TAO to stake. wait_for_inclusion (bool): Waits for the transaction to be included in a block. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + safe_staking (bool): If true, enables price safety checks to protect against fluctuating prices. The stake + will only execute if the price change doesn't exceed the rate tolerance. Default is False. + allow_partial_stake (bool): If true and safe_staking is enabled, allows partial staking when + the full amount would exceed the price threshold. If false, the entire stake fails if it would + exceed the threshold. Default is False. + rate_tolerance (float): The maximum allowed price change ratio when staking. For example, + 0.005 = 0.5% maximum price increase. Only used when safe_staking is True. Default is 0.005. Returns: bool: ``True`` if the staking is successful, False otherwise. - This function enables neurons to increase their stake in the network, enhancing their influence and potential - rewards in line with Bittensor's consensus and reward mechanisms. + This function enables neurons to increase their stake in the network, enhancing their influence and potential. + When safe_staking is enabled, it provides protection against price fluctuations during the time stake is + executed and the time it is actually processed by the chain. """ amount = check_and_convert_to_balance(amount) return await add_stake_extrinsic( @@ -2839,6 +2859,9 @@ async def add_stake( amount=amount, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + safe_staking=safe_staking, + allow_partial_stake=allow_partial_stake, + rate_tolerance=rate_tolerance, ) async def add_stake_multiple( @@ -2901,6 +2924,14 @@ async def burned_register( bool: ``True`` if the registration is successful, False otherwise. """ async with self: + if netuid == 0: + return await root_register_extrinsic( + subtensor=self, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + return await burned_register_extrinsic( subtensor=self, wallet=wallet, @@ -3191,35 +3222,6 @@ async def root_register( Returns: `True` if registration was successful, otherwise `False`. """ - netuid = 0 - logging.info( - f"Registering on netuid [blue]0[/blue] on network: [blue]{self.network}[/blue]" - ) - - # Check current recycle amount - logging.info("Fetching recycle amount & balance.") - block_hash = block_hash if block_hash else await self.get_block_hash() - - try: - recycle_call, balance = await asyncio.gather( - self.get_hyperparameter( - param_name="Burn", netuid=netuid, block_hash=block_hash - ), - self.get_balance(wallet.coldkeypub.ss58_address, block_hash=block_hash), - ) - except TypeError as e: - logging.error(f"Unable to retrieve current recycle. {e}") - return False - - current_recycle = Balance.from_rao(int(recycle_call)) - - # Check balance is sufficient - if balance < current_recycle: - logging.error( - f"[red]Insufficient balance {balance} to register neuron. " - f"Current recycle is {current_recycle} TAO[/red]." - ) - return False return await root_register_extrinsic( subtensor=self, @@ -3267,6 +3269,82 @@ async def root_set_weights( wait_for_inclusion=wait_for_inclusion, ) + async def set_delegate_take( + self, + wallet: "Wallet", + hotkey_ss58: str, + take: float, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + raise_error: bool = False, + ) -> tuple[bool, str]: + """ + Sets the delegate 'take' percentage for a neuron identified by its hotkey. + The 'take' represents the percentage of rewards that the delegate claims from its nominators' stakes. + + Arguments: + wallet (bittensor_wallet.Wallet): bittensor wallet instance. + hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. + take (float): Percentage reward for the delegate. + wait_for_inclusion (bool): Waits for the transaction to be included in a block. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + raise_error: Raises relevant exception rather than returning `False` if unsuccessful. + + Returns: + tuple[bool, str]: A tuple where the first element is a boolean indicating success or failure of the + operation, and the second element is a message providing additional information. + + Raises: + DelegateTakeTooHigh: Delegate take is too high. + DelegateTakeTooLow: Delegate take is too low. + DelegateTxRateLimitExceeded: A transactor exceeded the rate limit for delegate transaction. + HotKeyAccountNotExists: The hotkey does not exists. + NonAssociatedColdKey: Request to stake, unstake or subscribe is made by a coldkey that is not associated with the hotkey account. + bittensor_wallet.errors.PasswordError: Decryption failed or wrong password for decryption provided. + bittensor_wallet.errors.KeyFileError: Failed to decode keyfile data. + + The delegate take is a critical parameter in the network's incentive structure, influencing the distribution of + rewards among neurons and their nominators. + """ + + # u16 representation of the take + take_u16 = int(take * 0xFFFF) + + current_take = await self.get_delegate_take(hotkey_ss58) + current_take_u16 = int(current_take * 0xFFFF) + + if current_take_u16 == take_u16: + logging.info(":white_heavy_check_mark: [green]Already Set[/green]") + return True, "" + + logging.info(f"Updating {hotkey_ss58} take: current={current_take} new={take}") + + if current_take_u16 < take_u16: + success, error = await increase_take_extrinsic( + self, + wallet, + hotkey_ss58, + take_u16, + wait_for_finalization=wait_for_finalization, + wait_for_inclusion=wait_for_inclusion, + raise_error=raise_error, + ) + else: + success, error = await decrease_take_extrinsic( + self, + wallet, + hotkey_ss58, + take_u16, + wait_for_finalization=wait_for_finalization, + wait_for_inclusion=wait_for_inclusion, + raise_error=raise_error, + ) + + if success: + logging.info(":white_heavy_check_mark: [green]Take Updated[/green]") + + return success, error + async def set_subnet_identity( self, wallet: "Wallet", @@ -3460,6 +3538,9 @@ async def swap_stake( amount: Balance, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + safe_staking: bool = False, + allow_partial_stake: bool = False, + rate_tolerance: float = 0.005, ) -> bool: """ Moves stake between subnets while keeping the same coldkey-hotkey pair ownership. @@ -3473,9 +3554,25 @@ async def swap_stake( amount (Union[Balance, float]): The amount to swap. wait_for_inclusion (bool): Waits for the transaction to be included in a block. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + safe_staking (bool): If true, enables price safety checks to protect against fluctuating prices. The swap + will only execute if the price ratio between subnets doesn't exceed the rate tolerance. + Default is False. + allow_partial_stake (bool): If true and safe_staking is enabled, allows partial stake swaps when + the full amount would exceed the price threshold. If false, the entire swap fails if it would + exceed the threshold. Default is False. + rate_tolerance (float): The maximum allowed increase in the price ratio between subnets + (origin_price/destination_price). For example, 0.005 = 0.5% maximum increase. Only used + when safe_staking is True. Default is 0.005. Returns: success (bool): True if the extrinsic was successful. + + The price ratio for swap_stake in safe mode is calculated as: origin_subnet_price / destination_subnet_price + When safe_staking is enabled, the swap will only execute if: + - With allow_partial_stake=False: The entire swap amount can be executed without the price ratio + increasing more than rate_tolerance + - With allow_partial_stake=True: A partial amount will be swapped up to the point where the + price ratio would increase by rate_tolerance """ amount = check_and_convert_to_balance(amount) return await swap_stake_extrinsic( @@ -3487,6 +3584,9 @@ async def swap_stake( amount=amount, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + safe_staking=safe_staking, + allow_partial_stake=allow_partial_stake, + rate_tolerance=rate_tolerance, ) async def transfer_stake( @@ -3575,6 +3675,9 @@ async def unstake( amount: Optional[Balance] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + safe_staking: bool = False, + allow_partial_stake: bool = False, + rate_tolerance: float = 0.005, ) -> bool: """ Removes a specified amount of stake from a single hotkey account. This function is critical for adjusting @@ -3584,10 +3687,17 @@ async def unstake( wallet (bittensor_wallet.wallet): The wallet associated with the neuron from which the stake is being removed. hotkey_ss58 (Optional[str]): The ``SS58`` address of the hotkey account to unstake from. - netuid (Optional[int]): Subnet unique ID. + netuid (Optional[int]): The unique identifier of the subnet. amount (Balance): The amount of TAO to unstake. If not specified, unstakes all. wait_for_inclusion (bool): Waits for the transaction to be included in a block. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + safe_staking (bool): If true, enables price safety checks to protect against fluctuating prices. The unstake + will only execute if the price change doesn't exceed the rate tolerance. Default is False. + allow_partial_stake (bool): If true and safe_staking is enabled, allows partial unstaking when + the full amount would exceed the price threshold. If false, the entire unstake fails if it would + exceed the threshold. Default is False. + rate_tolerance (float): The maximum allowed price change ratio when unstaking. For example, + 0.005 = 0.5% maximum price decrease. Only used when safe_staking is True. Default is 0.005. Returns: bool: ``True`` if the unstaking process is successful, False otherwise. @@ -3604,6 +3714,9 @@ async def unstake( amount=amount, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + safe_staking=safe_staking, + allow_partial_stake=allow_partial_stake, + rate_tolerance=rate_tolerance, ) async def unstake_multiple( diff --git a/bittensor/core/chain_data/dynamic_info.py b/bittensor/core/chain_data/dynamic_info.py index 9f2739560b..cde40a0b79 100644 --- a/bittensor/core/chain_data/dynamic_info.py +++ b/bittensor/core/chain_data/dynamic_info.py @@ -77,9 +77,9 @@ def _from_dict(cls, decoded: dict) -> "DynamicInfo": price = ( Balance.from_tao(1.0) if netuid == 0 - else Balance.from_tao(tao_in.tao / alpha_in.tao) + else Balance.from_tao(tao_in.tao / alpha_in.tao).set_unit(netuid) if alpha_in.tao > 0 - else Balance.from_tao(1) + else Balance.from_tao(1).set_unit(netuid) ) # Root always has 1-1 price if decoded.get("subnet_identity"): diff --git a/bittensor/core/errors.py b/bittensor/core/errors.py index 9f856d15e6..0625a098f9 100644 --- a/bittensor/core/errors.py +++ b/bittensor/core/errors.py @@ -17,6 +17,21 @@ ExtrinsicNotFound = ExtrinsicNotFound +class _ChainErrorMeta(type): + _exceptions: dict[str, Exception] = {} + + def __new__(mcs, name, bases, attrs): + cls = super().__new__(mcs, name, bases, attrs) + + mcs._exceptions.setdefault(cls.__name__, cls) + + return cls + + @classmethod + def get_exception_class(mcs, exception_name): + return mcs._exceptions[exception_name] + + class MaxSuccessException(Exception): """Raised when the POW Solver has reached the max number of successful solutions.""" @@ -25,9 +40,20 @@ class MaxAttemptsException(Exception): """Raised when the POW Solver has reached the max number of attempts.""" -class ChainError(SubstrateRequestException): +class ChainError(SubstrateRequestException, metaclass=_ChainErrorMeta): """Base error for any chain related errors.""" + @classmethod + def from_error(cls, error): + try: + error_cls = _ChainErrorMeta.get_exception_class( + error["name"], + ) + except KeyError: + return cls(error) + else: + return error_cls(" ".join(error["docs"])) + class ChainConnectionError(ChainError): """Error for any chain connection related errors.""" @@ -41,6 +67,36 @@ class ChainQueryError(ChainError): """Error for any chain query related errors.""" +class DelegateTakeTooHigh(ChainTransactionError): + """ + Delegate take is too high. + """ + + +class DelegateTakeTooLow(ChainTransactionError): + """ + Delegate take is too low. + """ + + +class DelegateTxRateLimitExceeded(ChainTransactionError): + """ + A transactor exceeded the rate limit for delegate transaction. + """ + + +class HotKeyAccountNotExists(ChainTransactionError): + """ + The hotkey does not exist. + """ + + +class NonAssociatedColdKey(ChainTransactionError): + """ + Request to stake, unstake or subscribe is made by a coldkey that is not associated with the hotkey account. + """ + + class StakeError(ChainTransactionError): """Error raised when a stake transaction fails.""" diff --git a/bittensor/core/extrinsics/asyncex/move_stake.py b/bittensor/core/extrinsics/asyncex/move_stake.py index 4477f5d960..6ab0b13028 100644 --- a/bittensor/core/extrinsics/asyncex/move_stake.py +++ b/bittensor/core/extrinsics/asyncex/move_stake.py @@ -160,6 +160,9 @@ async def swap_stake_extrinsic( amount: Balance, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + safe_staking: bool = False, + allow_partial_stake: bool = False, + rate_tolerance: float = 0.005, ) -> bool: """ Swaps stake from one subnet to another for a given hotkey in the Bittensor network. @@ -173,6 +176,9 @@ async def swap_stake_extrinsic( amount (Balance): The amount of stake to swap as a `Balance` object. wait_for_inclusion (bool): If True, waits for transaction inclusion in a block. Defaults to True. wait_for_finalization (bool): If True, waits for transaction finalization. Defaults to False. + safe_staking (bool): If true, enables price safety checks to protect against price impact. + allow_partial_stake (bool): If true, allows partial stake swaps when the full amount would exceed the price tolerance. + rate_tolerance (float): Maximum allowed increase in price ratio (0.005 = 0.5%). Returns: bool: True if the swap was successful, False otherwise. @@ -205,20 +211,47 @@ async def swap_stake_extrinsic( return False try: - logging.info( - f"Swapping stake for hotkey [blue]{hotkey_ss58}[/blue]\n" - f"Amount: [green]{amount}[/green] from netuid [yellow]{origin_netuid}[/yellow] to netuid " - f"[yellow]{destination_netuid}[/yellow]" - ) + call_params = { + "hotkey": hotkey_ss58, + "origin_netuid": origin_netuid, + "destination_netuid": destination_netuid, + "alpha_amount": amount.rao, + } + + if safe_staking: + origin_pool, destination_pool = await asyncio.gather( + subtensor.subnet(netuid=origin_netuid), + subtensor.subnet(netuid=destination_netuid), + ) + swap_rate_ratio = origin_pool.price.rao / destination_pool.price.rao + swap_rate_ratio_with_tolerance = swap_rate_ratio * (1 + rate_tolerance) + + logging.info( + f"Swapping stake with safety for hotkey [blue]{hotkey_ss58}[/blue]\n" + f"Amount: [green]{amount}[/green] from netuid [green]{origin_netuid}[/green] to netuid " + f"[green]{destination_netuid}[/green]\n" + f"Current price ratio: [green]{swap_rate_ratio:.4f}[/green], " + f"Ratio with tolerance: [green]{swap_rate_ratio_with_tolerance:.4f}[/green]" + ) + call_params.update( + { + "limit_price": swap_rate_ratio_with_tolerance, + "allow_partial": allow_partial_stake, + } + ) + call_function = "swap_stake_limit" + else: + logging.info( + f"Swapping stake for hotkey [blue]{hotkey_ss58}[/blue]\n" + f"Amount: [green]{amount}[/green] from netuid [green]{origin_netuid}[/green] to netuid " + f"[green]{destination_netuid}[/green]" + ) + call_function = "swap_stake" + call = await subtensor.substrate.compose_call( call_module="SubtensorModule", - call_function="swap_stake", - call_params={ - "hotkey": hotkey_ss58, - "origin_netuid": origin_netuid, - "destination_netuid": destination_netuid, - "alpha_amount": amount.rao, - }, + call_function=call_function, + call_params=call_params, ) success, err_msg = await subtensor.sign_and_send_extrinsic( @@ -253,7 +286,12 @@ async def swap_stake_extrinsic( return True else: - logging.error(f":cross_mark: [red]Failed[/red]: {err_msg}") + if safe_staking and "Custom error: 8" in err_msg: + logging.error( + ":cross_mark: [red]Failed[/red]: Price ratio exceeded tolerance limit. Either increase price tolerance or enable partial staking." + ) + else: + logging.error(f":cross_mark: [red]Failed[/red]: {err_msg}") return False except Exception as e: diff --git a/bittensor/core/extrinsics/asyncex/root.py b/bittensor/core/extrinsics/asyncex/root.py index 61f9455988..6c30f631eb 100644 --- a/bittensor/core/extrinsics/asyncex/root.py +++ b/bittensor/core/extrinsics/asyncex/root.py @@ -7,6 +7,7 @@ from bittensor.core.errors import SubstrateRequestException from bittensor.utils import u16_normalized_float, format_error_message, unlock_key +from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging from bittensor.utils.weight_utils import ( normalize_max_weight, @@ -62,6 +63,33 @@ async def root_register_extrinsic( the response is `True`. """ netuid = 0 + logging.info( + f"Registering on netuid [blue]{netuid}[/blue] on network: [blue]{subtensor.network}[/blue]" + ) + + logging.info("Fetching recycle amount & balance.") + block_hash = await subtensor.get_block_hash() + recycle_call, balance = await asyncio.gather( + subtensor.get_hyperparameter( + param_name="Burn", + netuid=netuid, + block_hash=block_hash, + ), + subtensor.get_balance( + wallet.coldkeypub.ss58_address, + block_hash=block_hash, + ), + ) + + current_recycle = Balance.from_rao(int(recycle_call)) + + if balance < current_recycle: + logging.error( + f"[red]Insufficient balance {balance} to register neuron. " + f"Current recycle is {current_recycle} TAO[/red]." + ) + return False + if not (unlock := unlock_key(wallet)).success: logging.error(unlock.message) return False diff --git a/bittensor/core/extrinsics/asyncex/staking.py b/bittensor/core/extrinsics/asyncex/staking.py index 36e7d3f6d5..d3e22f1905 100644 --- a/bittensor/core/extrinsics/asyncex/staking.py +++ b/bittensor/core/extrinsics/asyncex/staking.py @@ -21,6 +21,9 @@ async def add_stake_extrinsic( amount: Optional[Balance] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + safe_staking: bool = False, + allow_partial_stake: bool = False, + rate_tolerance: float = 0.005, ) -> bool: """ Adds the specified amount of stake to passed hotkey `uid`. @@ -36,6 +39,9 @@ async def add_stake_extrinsic( `False` if the extrinsic fails to enter the block within the timeout. wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, or returns `False` if the extrinsic fails to be finalized within the timeout. + safe_staking: If set, uses safe staking logic + allow_partial_stake: If set, allows partial stake + rate_tolerance: The rate tolerance for safe staking Returns: success: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for @@ -97,19 +103,48 @@ async def add_stake_extrinsic( return False try: - logging.info( - f":satellite: [magenta]Staking to:[/magenta] " - f"[blue]netuid: {netuid}, amount: {staking_balance} " - f"on {subtensor.network}[/blue] [magenta]...[/magenta]" - ) + call_params = { + "hotkey": hotkey_ss58, + "netuid": netuid, + "amount_staked": staking_balance.rao, + } + + if safe_staking: + pool = await subtensor.subnet(netuid=netuid) + base_price = pool.price.rao + price_with_tolerance = base_price * (1 + rate_tolerance) + call_params.update( + { + "limit_price": price_with_tolerance, + "allow_partial": allow_partial_stake, + } + ) + call_function = "add_stake_limit" + + # For logging + base_rate = pool.price.tao + rate_with_tolerance = base_rate * (1 + rate_tolerance) + logging.info( + f":satellite: [magenta]Safe Staking to:[/magenta] " + f"[blue]netuid: [green]{netuid}[/green], amount: [green]{staking_balance}[/green], " + f"tolerance percentage: [green]{rate_tolerance*100}%[/green], " + f"price limit: [green]{rate_with_tolerance}[/green], " + f"original price: [green]{base_rate}[/green], " + f"with partial stake: [green]{allow_partial_stake}[/green] " + f"on [blue]{subtensor.network}[/blue][/magenta]...[/magenta]" + ) + else: + logging.info( + f":satellite: [magenta]Staking to:[/magenta] " + f"[blue]netuid: [green]{netuid}[/green], amount: [green]{staking_balance}[/green] " + f"on [blue]{subtensor.network}[/blue][magenta]...[/magenta]" + ) + call_function = "add_stake" + call = await subtensor.substrate.compose_call( call_module="SubtensorModule", - call_function="add_stake", - call_params={ - "hotkey": hotkey_ss58, - "amount_staked": staking_balance.rao, - "netuid": netuid, - }, + call_function=call_function, + call_params=call_params, ) staking_response, err_msg = await subtensor.sign_and_send_extrinsic( call, @@ -152,7 +187,12 @@ async def add_stake_extrinsic( ) return True else: - logging.error(f":cross_mark: [red]Failed: {err_msg}.[/red]") + if safe_staking and "Custom error: 8" in err_msg: + logging.error( + ":cross_mark: [red]Failed[/red]: Price exceeded tolerance limit. Either increase price tolerance or enable partial staking." + ) + else: + logging.error(f":cross_mark: [red]Failed: {err_msg}.[/red]") return False except NotRegisteredError: diff --git a/bittensor/core/extrinsics/asyncex/take.py b/bittensor/core/extrinsics/asyncex/take.py new file mode 100644 index 0000000000..6a51239bc0 --- /dev/null +++ b/bittensor/core/extrinsics/asyncex/take.py @@ -0,0 +1,72 @@ +from typing import TYPE_CHECKING + +from bittensor_wallet.bittensor_wallet import Wallet + +from bittensor.utils import unlock_key + +if TYPE_CHECKING: + from bittensor.core.async_subtensor import AsyncSubtensor + + +async def increase_take_extrinsic( + subtensor: "AsyncSubtensor", + wallet: Wallet, + hotkey_ss58: str, + take: int, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + raise_error: bool = False, +) -> tuple[bool, str]: + unlock = unlock_key(wallet, raise_error=raise_error) + + if not unlock.success: + return False, unlock.message + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="increase_take", + call_params={ + "hotkey": hotkey_ss58, + "take": take, + }, + ) + + return await subtensor.sign_and_send_extrinsic( + call, + wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + raise_error=raise_error, + ) + + +async def decrease_take_extrinsic( + subtensor: "AsyncSubtensor", + wallet: Wallet, + hotkey_ss58: str, + take: int, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + raise_error: bool = False, +) -> tuple[bool, str]: + unlock = unlock_key(wallet, raise_error=raise_error) + + if not unlock.success: + return False, unlock.message + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="decrease_take", + call_params={ + "hotkey": hotkey_ss58, + "take": take, + }, + ) + + return await subtensor.sign_and_send_extrinsic( + call, + wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + raise_error=raise_error, + ) diff --git a/bittensor/core/extrinsics/asyncex/unstaking.py b/bittensor/core/extrinsics/asyncex/unstaking.py index 7ba1d120a2..031859c043 100644 --- a/bittensor/core/extrinsics/asyncex/unstaking.py +++ b/bittensor/core/extrinsics/asyncex/unstaking.py @@ -20,6 +20,9 @@ async def unstake_extrinsic( amount: Optional[Balance] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + safe_staking: bool = False, + allow_partial_stake: bool = False, + rate_tolerance: float = 0.005, ) -> bool: """Removes stake into the wallet coldkey from the specified hotkey ``uid``. @@ -34,6 +37,9 @@ async def unstake_extrinsic( returns ``False`` if the extrinsic fails to enter the block within the timeout. wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning ``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout. + safe_staking: If true, enables price safety checks + allow_partial_stake: If true, allows partial unstaking if price tolerance exceeded + rate_tolerance: Maximum allowed price decrease percentage (0.005 = 0.5%) Returns: success (bool): Flag is ``True`` if extrinsic was finalized or included in the block. If we did not wait for @@ -83,19 +89,49 @@ async def unstake_extrinsic( return False try: - logging.info( - f"Unstaking [blue]{unstaking_balance}[/blue] from hotkey: [magenta]{hotkey_ss58}[/magenta] on netuid: " - f"[blue]{netuid}[/blue]" - ) + call_params = { + "hotkey": hotkey_ss58, + "netuid": netuid, + "amount_unstaked": unstaking_balance.rao, + } + if safe_staking: + pool = await subtensor.subnet(netuid=netuid) + base_price = pool.price.rao + price_with_tolerance = base_price * (1 - rate_tolerance) + + # For logging + base_rate = pool.price.tao + rate_with_tolerance = base_rate * (1 - rate_tolerance) + + logging.info( + f":satellite: [magenta]Safe Unstaking from:[/magenta] " + f"netuid: [green]{netuid}[/green], amount: [green]{unstaking_balance}[/green], " + f"tolerance percentage: [green]{rate_tolerance*100}%[/green], " + f"price limit: [green]{rate_with_tolerance}[/green], " + f"original price: [green]{base_rate}[/green], " + f"with partial unstake: [green]{allow_partial_stake}[/green] " + f"on [blue]{subtensor.network}[/blue][magenta]...[/magenta]" + ) + + call_params.update( + { + "limit_price": price_with_tolerance, + "allow_partial": allow_partial_stake, + } + ) + call_function = "remove_stake_limit" + else: + logging.info( + f":satellite: [magenta]Unstaking from:[/magenta] " + f"netuid: [green]{netuid}[/green], amount: [green]{unstaking_balance}[/green] " + f"on [blue]{subtensor.network}[/blue][magenta]...[/magenta]" + ) + call_function = "remove_stake" call = await subtensor.substrate.compose_call( call_module="SubtensorModule", - call_function="remove_stake", - call_params={ - "hotkey": hotkey_ss58, - "amount_unstaked": unstaking_balance.rao, - "netuid": netuid, - }, + call_function=call_function, + call_params=call_params, ) staking_response, err_msg = await subtensor.sign_and_send_extrinsic( call, @@ -138,7 +174,12 @@ async def unstake_extrinsic( ) return True else: - logging.error(f":cross_mark: [red]Failed: {err_msg}.[/red]") + if safe_staking and "Custom error: 8" in err_msg: + logging.error( + ":cross_mark: [red]Failed[/red]: Price exceeded tolerance limit. Either increase price tolerance or enable partial staking." + ) + else: + logging.error(f":cross_mark: [red]Failed: {err_msg}.[/red]") return False except NotRegisteredError: diff --git a/bittensor/core/extrinsics/move_stake.py b/bittensor/core/extrinsics/move_stake.py index 77cd4a986a..fdf9d406a8 100644 --- a/bittensor/core/extrinsics/move_stake.py +++ b/bittensor/core/extrinsics/move_stake.py @@ -157,6 +157,9 @@ def swap_stake_extrinsic( amount: Optional[Balance] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + safe_staking: bool = False, + allow_partial_stake: bool = False, + rate_tolerance: float = 0.005, ) -> bool: """ Moves stake between subnets while keeping the same coldkey-hotkey pair ownership. @@ -170,6 +173,9 @@ def swap_stake_extrinsic( amount (Union[Balance, float]): Amount to swap. wait_for_inclusion (bool): If true, waits for inclusion before returning. wait_for_finalization (bool): If true, waits for finalization before returning. + safe_staking (bool): If true, enables price safety checks to protect against price impact. + allow_partial_stake (bool): If true, allows partial stake swaps when the full amount would exceed the price tolerance. + rate_tolerance (float): Maximum allowed increase in price ratio (0.005 = 0.5%). Returns: success (bool): True if the swap was successful. @@ -203,20 +209,45 @@ def swap_stake_extrinsic( return False try: - logging.info( - f"Swapping stake for hotkey [blue]{hotkey_ss58}[/blue]\n" - f"Amount: [green]{amount}[/green] from netuid [yellow]{origin_netuid}[/yellow] to netuid " - f"[yellow]{destination_netuid}[/yellow]" - ) + call_params = { + "hotkey": hotkey_ss58, + "origin_netuid": origin_netuid, + "destination_netuid": destination_netuid, + "alpha_amount": amount.rao, + } + + if safe_staking: + origin_pool = subtensor.subnet(netuid=origin_netuid) + destination_pool = subtensor.subnet(netuid=destination_netuid) + swap_rate_ratio = origin_pool.price.rao / destination_pool.price.rao + swap_rate_ratio_with_tolerance = swap_rate_ratio * (1 + rate_tolerance) + + logging.info( + f"Swapping stake with safety for hotkey [blue]{hotkey_ss58}[/blue]\n" + f"Amount: [green]{amount}[/green] from netuid [green]{origin_netuid}[/green] to netuid " + f"[green]{destination_netuid}[/green]\n" + f"Current price ratio: [green]{swap_rate_ratio:.4f}[/green], " + f"Ratio with tolerance: [green]{swap_rate_ratio_with_tolerance:.4f}[/green]" + ) + call_params.update( + { + "limit_price": swap_rate_ratio_with_tolerance, + "allow_partial": allow_partial_stake, + } + ) + call_function = "swap_stake_limit" + else: + logging.info( + f"Swapping stake for hotkey [blue]{hotkey_ss58}[/blue]\n" + f"Amount: [green]{amount}[/green] from netuid [green]{origin_netuid}[/green] to netuid " + f"[green]{destination_netuid}[/green]" + ) + call_function = "swap_stake" + call = subtensor.substrate.compose_call( call_module="SubtensorModule", - call_function="swap_stake", - call_params={ - "hotkey": hotkey_ss58, - "origin_netuid": origin_netuid, - "destination_netuid": destination_netuid, - "alpha_amount": amount.rao, - }, + call_function=call_function, + call_params=call_params, ) success, err_msg = subtensor.sign_and_send_extrinsic( @@ -251,7 +282,12 @@ def swap_stake_extrinsic( return True else: - logging.error(f":cross_mark: [red]Failed[/red]: {err_msg}") + if safe_staking and "Custom error: 8" in err_msg: + logging.error( + ":cross_mark: [red]Failed[/red]: Price ratio exceeded tolerance limit. Either increase price tolerance or enable partial staking." + ) + else: + logging.error(f":cross_mark: [red]Failed[/red]: {err_msg}") return False except Exception as e: diff --git a/bittensor/core/extrinsics/root.py b/bittensor/core/extrinsics/root.py index bf49e8023a..43fd65a5a1 100644 --- a/bittensor/core/extrinsics/root.py +++ b/bittensor/core/extrinsics/root.py @@ -11,6 +11,7 @@ unlock_key, torch, ) +from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging from bittensor.utils.weight_utils import ( normalize_max_weight, @@ -65,6 +66,30 @@ def root_register_extrinsic( response is `True`. """ netuid = 0 + logging.info( + f"Registering on netuid [blue]{netuid}[/blue] on network: [blue]{subtensor.network}[/blue]" + ) + + logging.info("Fetching recycle amount & balance.") + block = subtensor.get_current_block() + recycle_call = subtensor.get_hyperparameter( + param_name="Burn", + netuid=netuid, + block=block, + ) + balance = subtensor.get_balance( + wallet.coldkeypub.ss58_address, + block=block, + ) + + current_recycle = Balance.from_rao(int(recycle_call)) + + if balance < current_recycle: + logging.error( + f"[red]Insufficient balance {balance} to register neuron. " + f"Current recycle is {current_recycle} TAO[/red]." + ) + return False if not (unlock := unlock_key(wallet)).success: logging.error(unlock.message) diff --git a/bittensor/core/extrinsics/staking.py b/bittensor/core/extrinsics/staking.py index 9872eee0f4..3e0b30e130 100644 --- a/bittensor/core/extrinsics/staking.py +++ b/bittensor/core/extrinsics/staking.py @@ -19,6 +19,9 @@ def add_stake_extrinsic( amount: Optional[Balance] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + safe_staking: bool = False, + allow_partial_stake: bool = False, + rate_tolerance: float = 0.005, ) -> bool: """ Adds the specified amount of stake to passed hotkey `uid`. @@ -33,6 +36,9 @@ def add_stake_extrinsic( `False` if the extrinsic fails to enter the block within the timeout. wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, or returns `False` if the extrinsic fails to be finalized within the timeout. + safe_staking (bool): If true, enables price safety checks + allow_partial_stake (bool): If true, allows partial unstaking if price tolerance exceeded + rate_tolerance (float): Maximum allowed price increase percentage (0.005 = 0.5%) Returns: success: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for @@ -91,20 +97,52 @@ def add_stake_extrinsic( return False try: - logging.info( - f":satellite: [magenta]Staking to:[/magenta] " - f"[blue]netuid: {netuid}, amount: {staking_balance} " - f"on {subtensor.network}[/blue] [magenta]...[/magenta]" - ) + call_params = { + "hotkey": hotkey_ss58, + "netuid": netuid, + "amount_staked": staking_balance.rao, + } + + if safe_staking: + pool = subtensor.subnet(netuid=netuid) + base_price = pool.price.rao + price_with_tolerance = base_price * (1 + rate_tolerance) + + # For logging + base_rate = pool.price.tao + rate_with_tolerance = base_rate * (1 + rate_tolerance) + + logging.info( + f":satellite: [magenta]Safe Staking to:[/magenta] " + f"[blue]netuid: [green]{netuid}[/green], amount: [green]{staking_balance}[/green], " + f"tolerance percentage: [green]{rate_tolerance*100}%[/green], " + f"price limit: [green]{rate_with_tolerance}[/green], " + f"original price: [green]{base_rate}[/green], " + f"with partial stake: [green]{allow_partial_stake}[/green] " + f"on [blue]{subtensor.network}[/blue][/magenta]...[/magenta]" + ) + + call_params.update( + { + "limit_price": price_with_tolerance, + "allow_partial": allow_partial_stake, + } + ) + call_function = "add_stake_limit" + else: + logging.info( + f":satellite: [magenta]Staking to:[/magenta] " + f"[blue]netuid: [green]{netuid}[/green], amount: [green]{staking_balance}[/green] " + f"on [blue]{subtensor.network}[/blue][magenta]...[/magenta]" + ) + call_function = "add_stake" + call = subtensor.substrate.compose_call( call_module="SubtensorModule", - call_function="add_stake", - call_params={ - "hotkey": hotkey_ss58, - "amount_staked": staking_balance.rao, - "netuid": netuid, - }, + call_function=call_function, + call_params=call_params, ) + staking_response, err_msg = subtensor.sign_and_send_extrinsic( call, wallet, @@ -143,7 +181,12 @@ def add_stake_extrinsic( ) return True else: - logging.error(":cross_mark: [red]Failed[/red]: Error unknown.") + if safe_staking and "Custom error: 8" in err_msg: + logging.error( + ":cross_mark: [red]Failed[/red]: Price exceeded tolerance limit. Either increase price tolerance or enable partial staking." + ) + else: + logging.error(f":cross_mark: [red]Failed: {err_msg}.[/red]") return False # TODO I don't think these are used. Maybe should just catch SubstrateRequestException? diff --git a/bittensor/core/extrinsics/take.py b/bittensor/core/extrinsics/take.py new file mode 100644 index 0000000000..1ac6e96040 --- /dev/null +++ b/bittensor/core/extrinsics/take.py @@ -0,0 +1,72 @@ +from typing import TYPE_CHECKING + +from bittensor_wallet.bittensor_wallet import Wallet + +from bittensor.utils import unlock_key + +if TYPE_CHECKING: + from bittensor.core.subtensor import Subtensor + + +def increase_take_extrinsic( + subtensor: "Subtensor", + wallet: Wallet, + hotkey_ss58: str, + take: int, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + raise_error: bool = False, +) -> tuple[bool, str]: + unlock = unlock_key(wallet, raise_error=raise_error) + + if not unlock.success: + return False, unlock.message + + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="increase_take", + call_params={ + "hotkey": hotkey_ss58, + "take": take, + }, + ) + + return subtensor.sign_and_send_extrinsic( + call, + wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + raise_error=raise_error, + ) + + +def decrease_take_extrinsic( + subtensor: "Subtensor", + wallet: Wallet, + hotkey_ss58: str, + take: int, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + raise_error: bool = False, +) -> tuple[bool, str]: + unlock = unlock_key(wallet, raise_error=raise_error) + + if not unlock.success: + return False, unlock.message + + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="decrease_take", + call_params={ + "hotkey": hotkey_ss58, + "take": take, + }, + ) + + return subtensor.sign_and_send_extrinsic( + call, + wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + raise_error=raise_error, + ) diff --git a/bittensor/core/extrinsics/unstaking.py b/bittensor/core/extrinsics/unstaking.py index eb178f56f9..edc2538902 100644 --- a/bittensor/core/extrinsics/unstaking.py +++ b/bittensor/core/extrinsics/unstaking.py @@ -19,6 +19,9 @@ def unstake_extrinsic( amount: Optional[Balance] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + safe_staking: bool = False, + allow_partial_stake: bool = False, + rate_tolerance: float = 0.005, ) -> bool: """Removes stake into the wallet coldkey from the specified hotkey ``uid``. @@ -33,6 +36,9 @@ def unstake_extrinsic( returns ``False`` if the extrinsic fails to enter the block within the timeout. wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning ``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout. + safe_staking: If true, enables price safety checks + allow_partial_stake: If true, allows partial unstaking if price tolerance exceeded + rate_tolerance: Maximum allowed price decrease percentage (0.005 = 0.5%) Returns: success (bool): Flag is ``True`` if extrinsic was finalized or included in the block. If we did not wait for @@ -79,18 +85,52 @@ def unstake_extrinsic( return False try: - logging.info( - f"Unstaking [blue]{unstaking_balance}[/blue] from [magenta]{hotkey_ss58}[/magenta] on [blue]{netuid}[/blue]" - ) + call_params = { + "hotkey": hotkey_ss58, + "netuid": netuid, + "amount_unstaked": unstaking_balance.rao, + } + + if safe_staking: + pool = subtensor.subnet(netuid=netuid) + base_price = pool.price.rao + price_with_tolerance = base_price * (1 - rate_tolerance) + + # For logging + base_rate = pool.price.tao + rate_with_tolerance = base_rate * (1 - rate_tolerance) + + logging.info( + f":satellite: [magenta]Safe Unstaking from:[/magenta] " + f"netuid: [green]{netuid}[/green], amount: [green]{unstaking_balance}[/green], " + f"tolerance percentage: [green]{rate_tolerance*100}%[/green], " + f"price limit: [green]{rate_with_tolerance}[/green], " + f"original price: [green]{base_rate}[/green], " + f"with partial unstake: [green]{allow_partial_stake}[/green] " + f"on [blue]{subtensor.network}[/blue][magenta]...[/magenta]" + ) + + call_params.update( + { + "limit_price": price_with_tolerance, + "allow_partial": allow_partial_stake, + } + ) + call_function = "remove_stake_limit" + else: + logging.info( + f":satellite: [magenta]Unstaking from:[/magenta] " + f"netuid: [green]{netuid}[/green], amount: [green]{unstaking_balance}[/green] " + f"on [blue]{subtensor.network}[/blue][magenta]...[/magenta]" + ) + call_function = "remove_stake" + call = subtensor.substrate.compose_call( call_module="SubtensorModule", - call_function="remove_stake", - call_params={ - "hotkey": hotkey_ss58, - "amount_unstaked": unstaking_balance.rao, - "netuid": netuid, - }, + call_function=call_function, + call_params=call_params, ) + staking_response, err_msg = subtensor.sign_and_send_extrinsic( call, wallet, @@ -130,7 +170,12 @@ def unstake_extrinsic( ) return True else: - logging.error(f":cross_mark: [red]Failed: {err_msg}.[/red]") + if safe_staking and "Custom error: 8" in err_msg: + logging.error( + ":cross_mark: [red]Failed[/red]: Price exceeded tolerance limit. Either increase price tolerance or enable partial staking." + ) + else: + logging.error(f":cross_mark: [red]Failed: {err_msg}.[/red]") return False except NotRegisteredError: diff --git a/bittensor/core/metagraph.py b/bittensor/core/metagraph.py index dad65986c9..5554b32a3d 100644 --- a/bittensor/core/metagraph.py +++ b/bittensor/core/metagraph.py @@ -1,3 +1,4 @@ +import asyncio import copy import os import pickle @@ -1397,13 +1398,13 @@ async def sync( # If not a 'lite' version, compute and set weights and bonds for each neuron if not lite: - await self._set_weights_and_bonds(subtensor=subtensor) + await self._set_weights_and_bonds(subtensor=subtensor, block=block) # Fills in the stake associated attributes of a class instance from a chain response. await self._get_all_stakes_from_chain(block=block) # apply MetagraphInfo data to instance - await self._apply_metagraph_info() + await self._apply_metagraph_info(block=block) async def _initialize_subtensor( self, subtensor: "AsyncSubtensor" @@ -1476,9 +1477,7 @@ async def _assign_neurons( self.neurons = await subtensor.neurons(block=block, netuid=self.netuid) self.lite = lite - async def _set_weights_and_bonds( - self, subtensor: Optional["AsyncSubtensor"] = None - ): + async def _set_weights_and_bonds(self, subtensor: "AsyncSubtensor", block: int): """ Computes and sets the weights and bonds for each neuron in the metagraph. This method is responsible for processing the raw weight and bond data obtained from the network and converting it into a structured format @@ -1499,6 +1498,7 @@ async def _set_weights_and_bonds( [neuron.weights for neuron in self.neurons], "weights", subtensor, + block=block, ) else: self.weights = self._process_weights_or_bonds( @@ -1509,7 +1509,7 @@ async def _set_weights_and_bonds( ) async def _process_root_weights( - self, data: list, attribute: str, subtensor: "AsyncSubtensor" + self, data: list, attribute: str, subtensor: "AsyncSubtensor", block: int ) -> Union[NDArray, "torch.nn.Parameter"]: """ Specifically processes the root weights data for the metagraph. This method is similar to :func:`_process_weights_or_bonds` @@ -1530,8 +1530,10 @@ async def _process_root_weights( self.root_weights = self._process_root_weights(raw_root_weights_data, "weights", subtensor) """ data_array = [] - n_subnets = await subtensor.get_total_subnets() or 0 - subnets = await subtensor.get_subnets() + n_subnets_, subnets = await asyncio.gather( + subtensor.get_total_subnets(block=block), subtensor.get_subnets(block=block) + ) + n_subnets = n_subnets_ or 0 for item in data: if len(item) == 0: if use_torch(): @@ -1612,9 +1614,11 @@ async def _get_all_stakes_from_chain(self, block: int): except (SubstrateRequestException, AttributeError) as e: logging.debug(e) - async def _apply_metagraph_info(self): + async def _apply_metagraph_info(self, block: int): """Retrieves metagraph information for a specific subnet and applies it using a mixin.""" - metagraph_info = await self.subtensor.get_metagraph_info(self.netuid) + metagraph_info = await self.subtensor.get_metagraph_info( + self.netuid, block=block + ) if metagraph_info: self._apply_metagraph_info_mixin(metagraph_info=metagraph_info) @@ -1711,13 +1715,13 @@ def sync( # If not a 'lite' version, compute and set weights and bonds for each neuron if not lite: - self._set_weights_and_bonds(subtensor=subtensor) + self._set_weights_and_bonds(subtensor=subtensor, block=block) # Fills in the stake associated attributes of a class instance from a chain response. self._get_all_stakes_from_chain(block=block) # apply MetagraphInfo data to instance - self._apply_metagraph_info() + self._apply_metagraph_info(block=block) def _initialize_subtensor(self, subtensor: "Subtensor") -> "Subtensor": """ @@ -1787,7 +1791,7 @@ def _assign_neurons(self, block: int, lite: bool, subtensor: "Subtensor"): self.neurons = subtensor.neurons(block=block, netuid=self.netuid) self.lite = lite - def _set_weights_and_bonds(self, subtensor: Optional["Subtensor"] = None): + def _set_weights_and_bonds(self, block: int, subtensor: "Subtensor"): """ Computes and sets the weights and bonds for each neuron in the metagraph. This method is responsible for processing the raw weight and bond data obtained from the network and converting it into a structured format @@ -1804,9 +1808,7 @@ def _set_weights_and_bonds(self, subtensor: Optional["Subtensor"] = None): """ if self.netuid == 0: self.weights = self._process_root_weights( - [neuron.weights for neuron in self.neurons], - "weights", - subtensor, + [neuron.weights for neuron in self.neurons], "weights", subtensor, block ) else: self.weights = self._process_weights_or_bonds( @@ -1817,7 +1819,7 @@ def _set_weights_and_bonds(self, subtensor: Optional["Subtensor"] = None): ) def _process_root_weights( - self, data: list, attribute: str, subtensor: "Subtensor" + self, data: list, attribute: str, subtensor: "Subtensor", block: int ) -> Union[NDArray, "torch.nn.Parameter"]: """ Specifically processes the root weights data for the metagraph. This method is similar to :func:`_process_weights_or_bonds` @@ -1838,8 +1840,8 @@ def _process_root_weights( self.root_weights = self._process_root_weights(raw_root_weights_data, "weights", subtensor) """ data_array = [] - n_subnets = subtensor.get_total_subnets() or 0 - subnets = subtensor.get_subnets() + n_subnets = subtensor.get_total_subnets(block=block) or 0 + subnets = subtensor.get_subnets(block=block) for item in data: if len(item) == 0: if use_torch(): @@ -1920,9 +1922,9 @@ def _get_all_stakes_from_chain(self, block: int): except (SubstrateRequestException, AttributeError) as e: logging.debug(e) - def _apply_metagraph_info(self): + def _apply_metagraph_info(self, block: int): """Retrieves metagraph information for a specific subnet and applies it using a mixin.""" - metagraph_info = self.subtensor.get_metagraph_info(self.netuid) + metagraph_info = self.subtensor.get_metagraph_info(self.netuid, block=block) if metagraph_info: self._apply_metagraph_info_mixin(metagraph_info=metagraph_info) diff --git a/bittensor/core/settings.py b/bittensor/core/settings.py index 53eb32266a..3e4174f950 100644 --- a/bittensor/core/settings.py +++ b/bittensor/core/settings.py @@ -1,4 +1,4 @@ -__version__ = "9.0.4" +__version__ = "9.1.0" import os import re diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 406074bb12..244080ff51 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -30,6 +30,7 @@ from bittensor.core.chain_data.chain_identity import ChainIdentity from bittensor.core.chain_data.utils import decode_metadata from bittensor.core.config import Config +from bittensor.core.errors import ChainError from bittensor.core.extrinsics.commit_reveal import commit_reveal_v3_extrinsic from bittensor.core.extrinsics.commit_weights import ( commit_weights_extrinsic, @@ -60,6 +61,10 @@ add_stake_extrinsic, add_stake_multiple_extrinsic, ) +from bittensor.core.extrinsics.take import ( + decrease_take_extrinsic, + increase_take_extrinsic, +) from bittensor.core.extrinsics.transfer import transfer_extrinsic from bittensor.core.extrinsics.unstaking import ( unstake_extrinsic, @@ -835,9 +840,7 @@ def get_delegate_identities( for ss58_address, identity in identities } - def get_delegate_take( - self, hotkey_ss58: str, block: Optional[int] = None - ) -> Optional[float]: + def get_delegate_take(self, hotkey_ss58: str, block: Optional[int] = None) -> float: """ Retrieves the delegate 'take' percentage for a neuron identified by its hotkey. The 'take' represents the percentage of rewards that the delegate claims from its nominators' stakes. @@ -847,7 +850,7 @@ def get_delegate_take( block (Optional[int]): The blockchain block number for the query. Returns: - Optional[float]: The delegate take percentage, None if not available. + float: The delegate take percentage. The delegate take is a critical parameter in the network's incentive structure, influencing the distribution of rewards among neurons and their nominators. @@ -857,11 +860,8 @@ def get_delegate_take( block=block, params=[hotkey_ss58], ) - return ( - None - if result is None - else u16_normalized_float(getattr(result, "value", 0)) - ) + + return u16_normalized_float(result.value) # type: ignore def get_delegated( self, coldkey_ss58: str, block: Optional[int] = None @@ -1572,6 +1572,82 @@ def immunity_period( ) return None if call is None else int(call) + def set_delegate_take( + self, + wallet: "Wallet", + hotkey_ss58: str, + take: float, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + raise_error: bool = False, + ) -> tuple[bool, str]: + """ + Sets the delegate 'take' percentage for a nueron identified by its hotkey. + The 'take' represents the percentage of rewards that the delegate claims from its nominators' stakes. + + Arguments: + wallet (bittensor_wallet.Wallet): bittensor wallet instance. + hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. + take (float): Percentage reward for the delegate. + wait_for_inclusion (bool): Waits for the transaction to be included in a block. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + raise_error: Raises relevant exception rather than returning `False` if unsuccessful. + + Returns: + tuple[bool, str]: A tuple where the first element is a boolean indicating success or failure of the + operation, and the second element is a message providing additional information. + + Raises: + DelegateTakeTooHigh: Delegate take is too high. + DelegateTakeTooLow: Delegate take is too low. + DelegateTxRateLimitExceeded: A transactor exceeded the rate limit for delegate transaction. + HotKeyAccountNotExists: The hotkey does not exists. + NonAssociatedColdKey: Request to stake, unstake or subscribe is made by a coldkey that is not associated with the hotkey account. + bittensor_wallet.errors.PasswordError: Decryption failed or wrong password for decryption provided. + bittensor_wallet.errors.KeyFileError: Failed to decode keyfile data. + + The delegate take is a critical parameter in the network's incentive structure, influencing the distribution of + rewards among neurons and their nominators. + """ + + # u16 representation of the take + take_u16 = int(take * 0xFFFF) + + current_take = self.get_delegate_take(hotkey_ss58) + current_take_u16 = int(current_take * 0xFFFF) + + if current_take_u16 == take_u16: + logging.info(":white_heavy_check_mark: [green]Already Set[/green]") + return True, "" + + logging.info(f"Updating {hotkey_ss58} take: current={current_take} new={take}") + + if current_take_u16 < take_u16: + success, error = increase_take_extrinsic( + self, + wallet, + hotkey_ss58, + take_u16, + wait_for_finalization=wait_for_finalization, + wait_for_inclusion=wait_for_inclusion, + raise_error=raise_error, + ) + else: + success, error = decrease_take_extrinsic( + self, + wallet, + hotkey_ss58, + take_u16, + wait_for_finalization=wait_for_finalization, + wait_for_inclusion=wait_for_inclusion, + raise_error=raise_error, + ) + + if success: + logging.info(":white_heavy_check_mark: [green]Take Updated[/green]") + + return success, error + def is_hotkey_delegate(self, hotkey_ss58: str, block: Optional[int] = None) -> bool: """ Determines whether a given hotkey (public key) is a delegate on the Bittensor network. This function checks if @@ -2066,6 +2142,7 @@ def sign_and_send_extrinsic( use_nonce: bool = False, period: Optional[int] = None, nonce_key: str = "hotkey", + raise_error: bool = False, ) -> tuple[bool, str]: """ Helper method to sign and submit an extrinsic call to chain. @@ -2076,6 +2153,7 @@ def sign_and_send_extrinsic( wait_for_inclusion (bool): whether to wait until the extrinsic call is included on the chain wait_for_finalization (bool): whether to wait until the extrinsic call is finalized on the chain sign_with: the wallet's keypair to use for the signing. Options are "coldkey", "hotkey", "coldkeypub" + raise_error: raises relevant exception rather than returning `False` if unsuccessful. Returns: (success, error message) @@ -2114,9 +2192,15 @@ def sign_and_send_extrinsic( if response.is_success: return True, "" + if raise_error: + raise ChainError.from_error(response.error_message) + return False, format_error_message(response.error_message) except SubstrateRequestException as e: + if raise_error: + raise + return False, format_error_message(e) # Extrinsics ======================================================================================================= @@ -2129,6 +2213,9 @@ def add_stake( amount: Optional[Balance] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + safe_staking: bool = False, + allow_partial_stake: bool = False, + rate_tolerance: float = 0.005, ) -> bool: """ Adds the specified amount of stake to a neuron identified by the hotkey ``SS58`` address. @@ -2142,12 +2229,21 @@ def add_stake( amount (Balance): The amount of TAO to stake. wait_for_inclusion (bool): Waits for the transaction to be included in a block. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + safe_staking (bool): If true, enables price safety checks to protect against fluctuating prices. The stake + will only execute if the price change doesn't exceed the rate tolerance. Default is False. + allow_partial_stake (bool): If true and safe_staking is enabled, allows partial staking when + the full amount would exceed the price tolerance. If false, the entire stake fails if it would + exceed the tolerance. Default is False. + rate_tolerance (float): The maximum allowed price change ratio when staking. For example, + 0.005 = 0.5% maximum price increase. Only used when safe_staking is True. Default is 0.005. Returns: - bool: ``True`` if the staking is successful, False otherwise. + bool: True if the staking is successful, False otherwise. This function enables neurons to increase their stake in the network, enhancing their influence and potential rewards in line with Bittensor's consensus and reward mechanisms. + When safe_staking is enabled, it provides protection against price fluctuations during the time stake is + executed and the time it is actually processed by the chain. """ amount = check_and_convert_to_balance(amount) return add_stake_extrinsic( @@ -2158,6 +2254,9 @@ def add_stake( amount=amount, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + safe_staking=safe_staking, + allow_partial_stake=allow_partial_stake, + rate_tolerance=rate_tolerance, ) def add_stake_multiple( @@ -2219,6 +2318,15 @@ def burned_register( Returns: bool: ``True`` if the registration is successful, False otherwise. """ + + if netuid == 0: + return root_register_extrinsic( + subtensor=self, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + return burned_register_extrinsic( subtensor=self, wallet=wallet, @@ -2506,32 +2614,6 @@ def root_register( Returns: `True` if registration was successful, otherwise `False`. """ - logging.info( - f"Registering on netuid [blue]0[/blue] on network: [blue]{self.network}[/blue]" - ) - - # Check current recycle amount - logging.info("Fetching recycle amount & balance.") - block = self.get_current_block() - - try: - recycle_call = cast( - int, self.get_hyperparameter(param_name="Burn", netuid=0, block=block) - ) - balance = self.get_balance(wallet.coldkeypub.ss58_address, block=block) - except TypeError as e: - logging.error(f"Unable to retrieve current recycle. {e}") - return False - - current_recycle = Balance.from_rao(int(recycle_call)) - - # Check balance is sufficient - if balance < current_recycle: - logging.error( - f"[red]Insufficient balance {balance} to register neuron. " - f"Current recycle is {current_recycle} TAO[/red]." - ) - return False return root_register_extrinsic( subtensor=self, @@ -2760,6 +2842,9 @@ def swap_stake( amount: Balance, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + safe_staking: bool = False, + allow_partial_stake: bool = False, + rate_tolerance: float = 0.005, ) -> bool: """ Moves stake between subnets while keeping the same coldkey-hotkey pair ownership. @@ -2773,9 +2858,26 @@ def swap_stake( amount (Union[Balance, float]): The amount to swap. wait_for_inclusion (bool): Waits for the transaction to be included in a block. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + safe_staking (bool): If true, enables price safety checks to protect against fluctuating prices. The swap + will only execute if the price ratio between subnets doesn't exceed the rate tolerance. + Default is False. + allow_partial_stake (bool): If true and safe_staking is enabled, allows partial stake swaps when + the full amount would exceed the price tolerance. If false, the entire swap fails if it would + exceed the tolerance. Default is False. + rate_tolerance (float): The maximum allowed increase in the price ratio between subnets + (origin_price/destination_price). For example, 0.005 = 0.5% maximum increase. Only used + when safe_staking is True. Default is 0.005. + Returns: success (bool): True if the extrinsic was successful. + + The price ratio for swap_stake in safe mode is calculated as: origin_subnet_price / destination_subnet_price + When safe_staking is enabled, the swap will only execute if: + - With allow_partial_stake=False: The entire swap amount can be executed without the price ratio + increasing more than rate_tolerance + - With allow_partial_stake=True: A partial amount will be swapped up to the point where the + price ratio would increase by rate_tolerance """ amount = check_and_convert_to_balance(amount) return swap_stake_extrinsic( @@ -2787,6 +2889,9 @@ def swap_stake( amount=amount, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + safe_staking=safe_staking, + allow_partial_stake=allow_partial_stake, + rate_tolerance=rate_tolerance, ) def transfer( @@ -2875,6 +2980,9 @@ def unstake( amount: Optional[Balance] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + safe_staking: bool = False, + allow_partial_stake: bool = False, + rate_tolerance: float = 0.005, ) -> bool: """ Removes a specified amount of stake from a single hotkey account. This function is critical for adjusting @@ -2888,12 +2996,20 @@ def unstake( amount (Balance): The amount of TAO to unstake. If not specified, unstakes all. wait_for_inclusion (bool): Waits for the transaction to be included in a block. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + safe_staking (bool): If true, enables price safety checks to protect against fluctuating prices. The unstake + will only execute if the price change doesn't exceed the rate tolerance. Default is False. + allow_partial_stake (bool): If true and safe_staking is enabled, allows partial unstaking when + the full amount would exceed the price tolerance. If false, the entire unstake fails if it would + exceed the tolerance. Default is False. + rate_tolerance (float): The maximum allowed price change ratio when unstaking. For example, + 0.005 = 0.5% maximum price decrease. Only used when safe_staking is True. Default is 0.005. Returns: bool: ``True`` if the unstaking process is successful, False otherwise. This function supports flexible stake management, allowing neurons to adjust their network participation and - potential reward accruals. + potential reward accruals. When safe_staking is enabled, it provides protection against price fluctuations + during the time unstake is executed and the time it is actually processed by the chain. """ amount = check_and_convert_to_balance(amount) return unstake_extrinsic( @@ -2904,6 +3020,9 @@ def unstake( amount=amount, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + safe_staking=safe_staking, + allow_partial_stake=allow_partial_stake, + rate_tolerance=rate_tolerance, ) def unstake_multiple( diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index c116cc0f84..534a2eedab 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -330,13 +330,25 @@ def validate_chain_endpoint(endpoint_url: str) -> tuple[bool, str]: return True, "" -def unlock_key(wallet: "Wallet", unlock_type="coldkey") -> "UnlockStatus": +def unlock_key( + wallet: "Wallet", + unlock_type="coldkey", + raise_error=False, +) -> "UnlockStatus": """ Attempts to decrypt a wallet's coldkey or hotkey + Args: wallet: a Wallet object unlock_type: the key type, 'coldkey' or 'hotkey' - Returns: UnlockStatus for success status of unlock, with error message if unsuccessful + raise_error: if False, will return (False, error msg), if True will raise the otherwise-caught exception. + + Returns: + UnlockStatus for success status of unlock, with error message if unsuccessful + + Raises: + bittensor_wallet.errors.PasswordError: incorrect password + bittensor_wallet.errors.KeyFileError: keyfile is corrupt, non-writable, or non-readable, or non-existent """ if unlock_type == "coldkey": unlocker = "unlock_coldkey" @@ -350,9 +362,15 @@ def unlock_key(wallet: "Wallet", unlock_type="coldkey") -> "UnlockStatus": getattr(wallet, unlocker)() return UnlockStatus(True, "") except PasswordError: + if raise_error: + raise + err_msg = f"The password used to decrypt your {unlock_type.capitalize()} keyfile is invalid." return UnlockStatus(False, err_msg) except KeyFileError: + if raise_error: + raise + err_msg = f"{unlock_type.capitalize()} keyfile is corrupt, non-writable, or non-readable, or non-existent." return UnlockStatus(False, err_msg) diff --git a/bittensor/utils/easy_imports.py b/bittensor/utils/easy_imports.py index 985aee1ed8..7718ceede0 100644 --- a/bittensor/utils/easy_imports.py +++ b/bittensor/utils/easy_imports.py @@ -67,11 +67,16 @@ ChainError, ChainQueryError, ChainTransactionError, + DelegateTakeTooHigh, + DelegateTakeTooLow, + DelegateTxRateLimitExceeded, + HotKeyAccountNotExists, IdentityError, InternalServerError, InvalidRequestNameError, MetadataError, NominationError, + NonAssociatedColdKey, NotDelegateError, NotRegisteredError, NotVerifiedException, diff --git a/pyproject.toml b/pyproject.toml index e618850dd7..c95f3ba4f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bittensor" -version = "9.0.4" +version = "9.1.0" description = "Bittensor" readme = "README.md" authors = [ diff --git a/tests/e2e_tests/test_commit_reveal_v3.py b/tests/e2e_tests/test_commit_reveal_v3.py index a31063cd47..52d2324c62 100644 --- a/tests/e2e_tests/test_commit_reveal_v3.py +++ b/tests/e2e_tests/test_commit_reveal_v3.py @@ -1,15 +1,12 @@ import re -import time import numpy as np import pytest from bittensor.utils.btlogging import logging -from bittensor.utils.balance import Balance from bittensor.utils.weight_utils import convert_weights_and_uids_for_emit from tests.e2e_tests.utils.chain_interactions import ( sudo_set_admin_utils, sudo_set_hyperparameter_bool, - sudo_set_hyperparameter_values, wait_interval, next_tempo, ) @@ -57,14 +54,16 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle logging.console.info("Commit reveal enabled") # Change the weights rate limit on the subnet - assert sudo_set_hyperparameter_values( + status, error = sudo_set_admin_utils( local_chain, alice_wallet, call_function="sudo_set_weights_set_rate_limit", call_params={"netuid": netuid, "weights_set_rate_limit": "0"}, - return_error_message=True, ) + assert error is None + assert status is True + # Verify weights rate limit was changed assert ( subtensor.get_subnet_hyperparameters(netuid=netuid).weights_rate_limit == 0 @@ -81,7 +80,6 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle alice_wallet, call_function="sudo_set_tempo", call_params={"netuid": netuid, "tempo": tempo_set}, - return_error_message=True, )[0] is True ) diff --git a/tests/e2e_tests/test_commit_weights.py b/tests/e2e_tests/test_commit_weights.py index cb6b7fd885..4d966c8c37 100644 --- a/tests/e2e_tests/test_commit_weights.py +++ b/tests/e2e_tests/test_commit_weights.py @@ -7,7 +7,6 @@ from tests.e2e_tests.utils.chain_interactions import ( sudo_set_admin_utils, sudo_set_hyperparameter_bool, - sudo_set_hyperparameter_values, wait_epoch, ) @@ -54,15 +53,18 @@ async def test_commit_and_reveal_weights_legacy(local_chain, subtensor, alice_wa assert ( subtensor.weights_rate_limit(netuid=netuid) > 0 ), "Weights rate limit is below 0" + # Lower the rate limit - assert sudo_set_hyperparameter_values( + status, error = sudo_set_admin_utils( local_chain, alice_wallet, call_function="sudo_set_weights_set_rate_limit", call_params={"netuid": netuid, "weights_set_rate_limit": "0"}, - return_error_message=True, ) + assert error is None + assert status is True + assert ( subtensor.get_subnet_hyperparameters(netuid=netuid).weights_rate_limit == 0 ), "Failed to set weights_rate_limit" @@ -77,7 +79,6 @@ async def test_commit_and_reveal_weights_legacy(local_chain, subtensor, alice_wa "netuid": netuid, "tempo": 100, }, - return_error_message=True, ) # Commit-reveal values @@ -192,15 +193,18 @@ async def test_commit_weights_uses_next_nonce(local_chain, subtensor, alice_wall assert ( subtensor.weights_rate_limit(netuid=netuid) > 0 ), "Weights rate limit is below 0" + # Lower the rate limit - assert sudo_set_hyperparameter_values( + status, error = sudo_set_admin_utils( local_chain, alice_wallet, call_function="sudo_set_weights_set_rate_limit", call_params={"netuid": netuid, "weights_set_rate_limit": "0"}, - return_error_message=True, ) + assert error is None + assert status is True + assert ( subtensor.get_subnet_hyperparameters(netuid=netuid).weights_rate_limit == 0 ), "Failed to set weights_rate_limit" diff --git a/tests/e2e_tests/test_delegate.py b/tests/e2e_tests/test_delegate.py index ae9c6b9cac..c5ea365611 100644 --- a/tests/e2e_tests/test_delegate.py +++ b/tests/e2e_tests/test_delegate.py @@ -1,16 +1,14 @@ import pytest +import bittensor from bittensor.core.chain_data.chain_identity import ChainIdentity -from bittensor.core.chain_data.delegate_info import DelegateInfo, DelegatedInfo +from bittensor.core.chain_data.delegate_info import DelegatedInfo, DelegateInfo from bittensor.utils.balance import Balance from tests.e2e_tests.utils.chain_interactions import ( - decrease_take, - increase_take, set_identity, sudo_set_admin_utils, ) - DEFAULT_DELEGATE_TAKE = 0.179995422293431 @@ -80,7 +78,7 @@ def test_identity(subtensor, alice_wallet, bob_wallet): ) -def test_change_take(local_chain, subtensor, alice_wallet): +def test_change_take(local_chain, subtensor, alice_wallet, bob_wallet): """ Tests: - Get default Delegate's take once registered in root subnet @@ -88,14 +86,13 @@ def test_change_take(local_chain, subtensor, alice_wallet): - Try corner cases (increase/decrease beyond allowed min/max) """ - success, error = decrease_take( - subtensor, - alice_wallet, - 0.1, - ) - - assert success is False - assert "`HotKeyAccountNotExists(Module)`" in error + with pytest.raises(bittensor.HotKeyAccountNotExists): + subtensor.set_delegate_take( + alice_wallet, + alice_wallet.hotkey.ss58_address, + 0.1, + raise_error=True, + ) subtensor.root_register( alice_wallet, @@ -103,51 +100,44 @@ def test_change_take(local_chain, subtensor, alice_wallet): wait_for_finalization=True, ) - assert ( - subtensor.get_delegate_take(alice_wallet.hotkey.ss58_address) - == DEFAULT_DELEGATE_TAKE - ) - - success, error = increase_take( - subtensor, - alice_wallet, - 0.5, - ) - - assert success is False - assert "`DelegateTakeTooHigh(Module)`" in error - - # increase_take but try to change from 0.18 to 0.1 - success, error = increase_take( - subtensor, - alice_wallet, - 0.1, - ) - - assert "`DelegateTakeTooLow(Module)`" in error - assert success is False + take = subtensor.get_delegate_take(alice_wallet.hotkey.ss58_address) - success, error = decrease_take( - subtensor, + assert take == DEFAULT_DELEGATE_TAKE + + with pytest.raises(bittensor.NonAssociatedColdKey): + subtensor.set_delegate_take( + bob_wallet, + alice_wallet.hotkey.ss58_address, + 0.1, + raise_error=True, + ) + + with pytest.raises(bittensor.DelegateTakeTooHigh): + subtensor.set_delegate_take( + alice_wallet, + alice_wallet.hotkey.ss58_address, + 0.5, + raise_error=True, + ) + + subtensor.set_delegate_take( alice_wallet, + alice_wallet.hotkey.ss58_address, 0.1, + raise_error=True, ) - assert success is True - assert error == "" - take = subtensor.get_delegate_take(alice_wallet.hotkey.ss58_address) assert take == 0.09999237048905166 - success, error = increase_take( - subtensor, - alice_wallet, - 0.15, - ) - - assert success is False - assert "`DelegateTxRateLimitExceeded(Module)`" in error + with pytest.raises(bittensor.DelegateTxRateLimitExceeded): + subtensor.set_delegate_take( + alice_wallet, + alice_wallet.hotkey.ss58_address, + 0.15, + raise_error=True, + ) take = subtensor.get_delegate_take(alice_wallet.hotkey.ss58_address) @@ -162,15 +152,13 @@ def test_change_take(local_chain, subtensor, alice_wallet): }, ) - success, error = increase_take( - subtensor, + subtensor.set_delegate_take( alice_wallet, + alice_wallet.hotkey.ss58_address, 0.15, + raise_error=True, ) - assert success is True - assert error == "" - take = subtensor.get_delegate_take(alice_wallet.hotkey.ss58_address) assert take == 0.14999618524452582 @@ -320,7 +308,6 @@ def test_nominator_min_required_stake(local_chain, subtensor, alice_wallet, bob_ call_params={ "min_stake": "100000000000000", }, - return_error_message=True, ) minimum_required_stake = subtensor.get_minimum_required_stake() diff --git a/tests/e2e_tests/test_dendrite.py b/tests/e2e_tests/test_dendrite.py index 36a6dfa786..b2e891ff73 100644 --- a/tests/e2e_tests/test_dendrite.py +++ b/tests/e2e_tests/test_dendrite.py @@ -6,7 +6,6 @@ from bittensor.utils.btlogging import logging from tests.e2e_tests.utils.chain_interactions import ( sudo_set_admin_utils, - sudo_set_hyperparameter_values, wait_epoch, ) @@ -44,11 +43,10 @@ async def test_dendrite(local_chain, subtensor, templates, alice_wallet, bob_wal "netuid": netuid, "max_allowed_validators": 1, }, - return_error_message=True, ) # update weights_set_rate_limit for fast-blocks - assert sudo_set_hyperparameter_values( + status, error = sudo_set_admin_utils( local_chain, alice_wallet, call_function="sudo_set_weights_set_rate_limit", @@ -56,9 +54,11 @@ async def test_dendrite(local_chain, subtensor, templates, alice_wallet, bob_wal "netuid": netuid, "weights_set_rate_limit": 10, }, - return_error_message=True, ) + assert error is None + assert status is True + # Register Bob to the network assert subtensor.burned_register( bob_wallet, netuid diff --git a/tests/e2e_tests/test_hotkeys.py b/tests/e2e_tests/test_hotkeys.py index c6b4445e41..126690d46c 100644 --- a/tests/e2e_tests/test_hotkeys.py +++ b/tests/e2e_tests/test_hotkeys.py @@ -6,6 +6,7 @@ ) +SET_CHILDREN_COOLDOWN_PERIOD = 15 SET_CHILDREN_RATE_LIMIT = 150 @@ -54,17 +55,13 @@ def test_hotkeys(subtensor, alice_wallet): ) -@pytest.mark.skip( - reason="""The behavior of set_children changes: Instead of setting children immediately, the children will be set in the subnet epoch after a cool down period (7200 blocks). -https://github.com/opentensor/subtensor/pull/1050 -""", -) @pytest.mark.asyncio async def test_children(subtensor, alice_wallet, bob_wallet): """ Tests: - Get default children (empty list) - Update children list + - Checking cooldown period - Trigger rate limit - Clear children list """ @@ -102,6 +99,20 @@ async def test_children(subtensor, alice_wallet, bob_wallet): assert error == "" assert success is True + set_children_block = subtensor.get_current_block() + + success, children, error = subtensor.get_children( + alice_wallet.hotkey.ss58_address, + block=set_children_block, + netuid=1, + ) + + assert success is True + assert children == [] + assert error == "" + + subtensor.wait_for_block(set_children_block + SET_CHILDREN_COOLDOWN_PERIOD) + await wait_epoch(subtensor, netuid=1) success, children, error = subtensor.get_children( @@ -128,7 +139,7 @@ async def test_children(subtensor, alice_wallet, bob_wallet): assert "`TxRateLimitExceeded(Module)`" in error assert success is False - subtensor.wait_for_block(subtensor.block + SET_CHILDREN_RATE_LIMIT) + subtensor.wait_for_block(set_children_block + SET_CHILDREN_RATE_LIMIT) success, error = set_children( subtensor, @@ -140,6 +151,8 @@ async def test_children(subtensor, alice_wallet, bob_wallet): assert error == "" assert success is True + subtensor.wait_for_block(subtensor.block + SET_CHILDREN_COOLDOWN_PERIOD) + await wait_epoch(subtensor, netuid=1) success, children, error = subtensor.get_children( diff --git a/tests/e2e_tests/test_incentive.py b/tests/e2e_tests/test_incentive.py index 5a404316f6..0467a0cd81 100644 --- a/tests/e2e_tests/test_incentive.py +++ b/tests/e2e_tests/test_incentive.py @@ -2,17 +2,13 @@ import pytest -from bittensor import Balance - from tests.e2e_tests.utils.chain_interactions import ( - sudo_set_hyperparameter_values, - wait_epoch, sudo_set_admin_utils, + wait_epoch, ) @pytest.mark.asyncio -@pytest.mark.parametrize("local_chain", [False], indirect=True) async def test_incentive(local_chain, subtensor, templates, alice_wallet, bob_wallet): """ Test the incentive mechanism and interaction of miners/validators @@ -35,21 +31,6 @@ async def test_incentive(local_chain, subtensor, templates, alice_wallet, bob_wa # Verify subnet created successfully assert subtensor.subnet_exists(netuid), "Subnet wasn't created successfully" - # Change tempo to 10 - tempo_set = 10 - assert ( - sudo_set_admin_utils( - local_chain, - alice_wallet, - call_function="sudo_set_tempo", - call_params={"netuid": netuid, "tempo": tempo_set}, - return_error_message=True, - )[0] - is True - ) - tempo = subtensor.get_subnet_hyperparameters(netuid=netuid).tempo - assert tempo_set == tempo - # Register Bob as a neuron on the subnet assert subtensor.burned_register( bob_wallet, netuid @@ -60,27 +41,9 @@ async def test_incentive(local_chain, subtensor, templates, alice_wallet, bob_wa len(subtensor.neurons(netuid=netuid)) == 2 ), "Alice & Bob not registered in the subnet" - # Add stake for Alice - assert subtensor.add_stake( - alice_wallet, - netuid=netuid, - amount=Balance.from_tao(1_000), - wait_for_inclusion=True, - wait_for_finalization=True, - ), "Failed to add stake for Alice" - # Wait for the first epoch to pass await wait_epoch(subtensor, netuid) - # Add further stake so validator permit is activated - assert subtensor.add_stake( - alice_wallet, - netuid=netuid, - amount=Balance.from_tao(1_000), - wait_for_inclusion=True, - wait_for_finalization=True, - ), "Failed to add stake for Alice" - # Get latest metagraph metagraph = subtensor.metagraph(netuid) @@ -91,6 +54,9 @@ async def test_incentive(local_chain, subtensor, templates, alice_wallet, bob_wa assert alice_neuron.dividends == 0 assert alice_neuron.stake.tao > 0 assert alice_neuron.validator_trust == 0 + assert alice_neuron.incentive == 0 + assert alice_neuron.consensus == 0 + assert alice_neuron.rank == 0 bob_neuron = metagraph.neurons[1] @@ -100,21 +66,24 @@ async def test_incentive(local_chain, subtensor, templates, alice_wallet, bob_wa assert bob_neuron.trust == 0 # update weights_set_rate_limit for fast-blocks - assert sudo_set_hyperparameter_values( + status, error = sudo_set_admin_utils( local_chain, alice_wallet, call_function="sudo_set_weights_set_rate_limit", call_params={"netuid": netuid, "weights_set_rate_limit": 10}, - return_error_message=True, ) + assert error is None + assert status is True + async with templates.miner(bob_wallet, netuid): - async with templates.validator(alice_wallet, netuid): - # Wait for the Validator to process and set_weights - await asyncio.sleep(5) + async with templates.validator(alice_wallet, netuid) as validator: + # wait for the Validator to process and set_weights + async with asyncio.timeout(60): + await validator.set_weights.wait() # Wait few epochs - await wait_epoch(subtensor, netuid, times=2) + await wait_epoch(subtensor, netuid, times=4) # Refresh metagraph metagraph = subtensor.metagraph(netuid) @@ -125,12 +94,15 @@ async def test_incentive(local_chain, subtensor, templates, alice_wallet, bob_wa assert alice_neuron.validator_permit is True assert alice_neuron.dividends == 1.0 assert alice_neuron.stake.tao > 0 - assert alice_neuron.validator_trust == 1 + assert alice_neuron.validator_trust > 0.99 + assert alice_neuron.incentive < 0.5 + assert alice_neuron.consensus < 0.5 + assert alice_neuron.rank < 0.5 bob_neuron = metagraph.neurons[1] - assert bob_neuron.incentive == 1 - assert bob_neuron.consensus == 1 - assert bob_neuron.rank == 1 + assert bob_neuron.incentive > 0.5 + assert bob_neuron.consensus > 0.5 + assert bob_neuron.rank > 0.5 assert bob_neuron.trust == 1 print("✅ Passed test_incentive") diff --git a/tests/e2e_tests/test_metagraph.py b/tests/e2e_tests/test_metagraph.py index 69da571573..f199cf44e3 100644 --- a/tests/e2e_tests/test_metagraph.py +++ b/tests/e2e_tests/test_metagraph.py @@ -6,7 +6,6 @@ from bittensor.core.chain_data.metagraph_info import MetagraphInfo from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging -from tests.e2e_tests.utils.chain_interactions import ANY_BALANCE NULL_KEY = tuple(bytearray(32)) @@ -204,14 +203,14 @@ def test_metagraph_info(subtensor, alice_wallet): subnet_emission=Balance(0), alpha_in=Balance.from_tao(10), alpha_out=Balance.from_tao(2), - tao_in=ANY_BALANCE, + tao_in=Balance.from_tao(10), alpha_out_emission=Balance.from_tao(1), alpha_in_emission=Balance(0), tao_in_emission=Balance(0), pending_alpha_emission=Balance.from_tao(0.820004577), pending_root_emission=Balance(0), subnet_volume=Balance(0), - moving_price=Balance.from_tao(0.000003000), + moving_price=Balance(0), rho=10, kappa=32767, min_allowed_weights=0.0, @@ -268,9 +267,9 @@ def test_metagraph_info(subtensor, alice_wallet): trust=[0.0], rank=[0.0], block_at_registration=(0,), - alpha_stake=[ANY_BALANCE], + alpha_stake=[Balance.from_tao(1.0)], tao_stake=[Balance(0)], - total_stake=[ANY_BALANCE], + total_stake=[Balance.from_tao(1.0)], tao_dividends_per_hotkey=[ ("5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", Balance(0)) ], diff --git a/tests/e2e_tests/test_set_weights.py b/tests/e2e_tests/test_set_weights.py index d124238b5f..8a279e3ccf 100644 --- a/tests/e2e_tests/test_set_weights.py +++ b/tests/e2e_tests/test_set_weights.py @@ -5,7 +5,6 @@ from bittensor.utils.weight_utils import convert_weights_and_uids_for_emit from tests.e2e_tests.utils.chain_interactions import ( sudo_set_hyperparameter_bool, - sudo_set_hyperparameter_values, sudo_set_admin_utils, wait_epoch, ) @@ -35,7 +34,6 @@ async def test_set_weights_uses_next_nonce(local_chain, subtensor, alice_wallet) alice_wallet, call_function="sudo_set_network_rate_limit", call_params={"rate_limit": "0"}, # No limit - return_error_message=True, ) # Set lock reduction interval sudo_set_admin_utils( @@ -43,7 +41,6 @@ async def test_set_weights_uses_next_nonce(local_chain, subtensor, alice_wallet) alice_wallet, call_function="sudo_set_lock_reduction_interval", call_params={"interval": "1"}, # 1 block # reduce lock every block - return_error_message=True, ) # Try to register the subnets @@ -84,15 +81,18 @@ async def test_set_weights_uses_next_nonce(local_chain, subtensor, alice_wallet) assert ( subtensor.weights_rate_limit(netuid=netuid) > 0 ), "Weights rate limit is below 0" + # Lower the rate limit - assert sudo_set_hyperparameter_values( + status, error = sudo_set_admin_utils( local_chain, alice_wallet, call_function="sudo_set_weights_set_rate_limit", call_params={"netuid": netuid, "weights_set_rate_limit": "0"}, - return_error_message=True, ) + assert error is None + assert status is True + assert ( subtensor.get_subnet_hyperparameters(netuid=netuid).weights_rate_limit == 0 ), "Failed to set weights_rate_limit" diff --git a/tests/e2e_tests/test_staking.py b/tests/e2e_tests/test_staking.py index 64d61f466d..11b7b45054 100644 --- a/tests/e2e_tests/test_staking.py +++ b/tests/e2e_tests/test_staking.py @@ -1,8 +1,10 @@ -import pytest - +from bittensor import logging from bittensor.core.chain_data.stake_info import StakeInfo from bittensor.utils.balance import Balance from tests.e2e_tests.utils.chain_interactions import ANY_BALANCE +from tests.helpers.helpers import ApproxBalance + +logging.enable_info() def test_single_operation(subtensor, alice_wallet, bob_wallet): @@ -131,9 +133,6 @@ def test_single_operation(subtensor, alice_wallet, bob_wallet): assert stake == Balance(0) -@pytest.mark.skip( - reason="add_stake_multiple and unstake_multiple doesn't return (just hangs)", -) def test_batch_operations(subtensor, alice_wallet, bob_wallet): """ Tests: @@ -213,7 +212,7 @@ def test_batch_operations(subtensor, alice_wallet, bob_wallet): ) assert balances == { - alice_wallet.coldkey.ss58_address: alice_balance, + alice_wallet.coldkey.ss58_address: ApproxBalance(alice_balance.rao), bob_wallet.coldkey.ss58_address: Balance.from_tao(999_998), } @@ -245,3 +244,275 @@ def test_batch_operations(subtensor, alice_wallet, bob_wallet): bob_wallet.coldkey.ss58_address: Balance.from_tao(999_998), } assert balances[alice_wallet.coldkey.ss58_address] > alice_balance + + +def test_safe_staking_scenarios(subtensor, alice_wallet, bob_wallet): + """ + Tests safe staking scenarios with different parameters. + + For both staking and unstaking: + 1. Fails with strict threshold (0.5%) and no partial staking + 2. Succeeds with strict threshold (0.5%) and partial staking allowed + 3. Succeeds with lenient threshold (10% and 30%) and no partial staking + """ + netuid = 2 + # Register root as Alice - the subnet owner and validator + assert subtensor.register_subnet(alice_wallet) + + # Verify subnet created successfully + assert subtensor.subnet_exists(netuid), "Subnet wasn't created successfully" + + subtensor.burned_register( + alice_wallet, + netuid=netuid, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + subtensor.burned_register( + bob_wallet, + netuid=netuid, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + initial_stake = subtensor.get_stake( + alice_wallet.coldkey.ss58_address, + bob_wallet.hotkey.ss58_address, + netuid=netuid, + ) + assert initial_stake == Balance(0) + + # Test Staking Scenarios + stake_amount = Balance.from_tao(100) + + # 1. Strict params - should fail + success = subtensor.add_stake( + alice_wallet, + bob_wallet.hotkey.ss58_address, + netuid=netuid, + amount=stake_amount, + wait_for_inclusion=True, + wait_for_finalization=True, + safe_staking=True, + rate_tolerance=0.005, # 0.5% + allow_partial_stake=False, + ) + assert success is False + + current_stake = subtensor.get_stake( + alice_wallet.coldkey.ss58_address, + bob_wallet.hotkey.ss58_address, + netuid=netuid, + ) + assert current_stake == Balance(0), "Stake should not change after failed attempt" + + # 2. Partial allowed - should succeed partially + success = subtensor.add_stake( + alice_wallet, + bob_wallet.hotkey.ss58_address, + netuid=netuid, + amount=stake_amount, + wait_for_inclusion=True, + wait_for_finalization=True, + safe_staking=True, + rate_tolerance=0.005, # 0.5% + allow_partial_stake=True, + ) + assert success is True + + partial_stake = subtensor.get_stake( + alice_wallet.coldkey.ss58_address, + bob_wallet.hotkey.ss58_address, + netuid=netuid, + ) + assert partial_stake > Balance(0), "Partial stake should be added" + assert ( + partial_stake < stake_amount + ), "Partial stake should be less than requested amount" + + # 3. Higher threshold - should succeed fully + amount = Balance.from_tao(100) + success = subtensor.add_stake( + alice_wallet, + bob_wallet.hotkey.ss58_address, + netuid=netuid, + amount=amount, + wait_for_inclusion=True, + wait_for_finalization=True, + safe_staking=True, + rate_tolerance=0.1, # 10% + allow_partial_stake=False, + ) + assert success is True + + full_stake = subtensor.get_stake( + alice_wallet.coldkey.ss58_address, + bob_wallet.hotkey.ss58_address, + netuid=netuid, + ) + + # Test Unstaking Scenarios + # 1. Strict params - should fail + success = subtensor.unstake( + alice_wallet, + bob_wallet.hotkey.ss58_address, + netuid=netuid, + amount=stake_amount, + wait_for_inclusion=True, + wait_for_finalization=True, + safe_staking=True, + rate_tolerance=0.005, # 0.5% + allow_partial_stake=False, + ) + assert success is False + + current_stake = subtensor.get_stake( + alice_wallet.coldkey.ss58_address, + bob_wallet.hotkey.ss58_address, + netuid=netuid, + ) + assert ( + current_stake == full_stake + ), "Stake should not change after failed unstake attempt" + + # 2. Partial allowed - should succeed partially + success = subtensor.unstake( + alice_wallet, + bob_wallet.hotkey.ss58_address, + netuid=netuid, + amount=current_stake, + wait_for_inclusion=True, + wait_for_finalization=True, + safe_staking=True, + rate_tolerance=0.005, # 0.5% + allow_partial_stake=True, + ) + assert success is True + + partial_unstake = subtensor.get_stake( + alice_wallet.coldkey.ss58_address, + bob_wallet.hotkey.ss58_address, + netuid=netuid, + ) + assert partial_unstake > Balance(0), "Some stake should remain" + + # 3. Higher threshold - should succeed fully + success = subtensor.unstake( + alice_wallet, + bob_wallet.hotkey.ss58_address, + netuid=netuid, + amount=partial_unstake, + wait_for_inclusion=True, + wait_for_finalization=True, + safe_staking=True, + rate_tolerance=0.3, # 30% + allow_partial_stake=False, + ) + assert success is True + + +def test_safe_swap_stake_scenarios(subtensor, alice_wallet, bob_wallet): + """ + Tests safe swap stake scenarios with different parameters. + + Tests: + 1. Fails with strict threshold (0.5%) + 2. Succeeds with lenient threshold (10%) + """ + # Create new subnet (netuid 2) and register Alice + origin_netuid = 2 + assert subtensor.register_subnet(bob_wallet) + assert subtensor.subnet_exists(origin_netuid), "Subnet wasn't created successfully" + dest_netuid = 3 + assert subtensor.register_subnet(bob_wallet) + assert subtensor.subnet_exists(dest_netuid), "Subnet wasn't created successfully" + + # Register Alice on both subnets + subtensor.burned_register( + alice_wallet, + netuid=origin_netuid, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + subtensor.burned_register( + alice_wallet, + netuid=dest_netuid, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + # Add initial stake to swap from + initial_stake_amount = Balance.from_tao(10_000) + success = subtensor.add_stake( + alice_wallet, + alice_wallet.hotkey.ss58_address, + netuid=origin_netuid, + amount=initial_stake_amount, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert success is True + + origin_stake = subtensor.get_stake( + alice_wallet.coldkey.ss58_address, + alice_wallet.hotkey.ss58_address, + netuid=origin_netuid, + ) + assert origin_stake > Balance(0), "Origin stake should be non-zero" + + stake_swap_amount = Balance.from_tao(10_000) + # 1. Try swap with strict threshold and big amount- should fail + success = subtensor.swap_stake( + wallet=alice_wallet, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + origin_netuid=origin_netuid, + destination_netuid=dest_netuid, + amount=stake_swap_amount, + wait_for_inclusion=True, + wait_for_finalization=True, + safe_staking=True, + rate_tolerance=0.005, # 0.5% + allow_partial_stake=False, + ) + assert success is False + + # Verify no stake was moved + dest_stake = subtensor.get_stake( + alice_wallet.coldkey.ss58_address, + alice_wallet.hotkey.ss58_address, + netuid=dest_netuid, + ) + assert dest_stake == Balance( + 0 + ), "Destination stake should remain 0 after failed swap" + + # 2. Try swap with higher threshold and less amount - should succeed + stake_swap_amount = Balance.from_tao(100) + success = subtensor.swap_stake( + wallet=alice_wallet, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + origin_netuid=origin_netuid, + destination_netuid=dest_netuid, + amount=stake_swap_amount, + wait_for_inclusion=True, + wait_for_finalization=True, + safe_staking=True, + rate_tolerance=0.3, # 30% + allow_partial_stake=True, + ) + assert success is True + + # Verify stake was moved + origin_stake = subtensor.get_stake( + alice_wallet.coldkey.ss58_address, + alice_wallet.hotkey.ss58_address, + netuid=origin_netuid, + ) + dest_stake = subtensor.get_stake( + alice_wallet.coldkey.ss58_address, + alice_wallet.hotkey.ss58_address, + netuid=dest_netuid, + ) + assert dest_stake > Balance( + 0 + ), "Destination stake should be non-zero after successful swap" diff --git a/tests/e2e_tests/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py index b80548041b..2f59dfd1f7 100644 --- a/tests/e2e_tests/utils/chain_interactions.py +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -150,8 +150,7 @@ def sudo_set_admin_utils( wallet: "Wallet", call_function: str, call_params: dict, - return_error_message: bool = False, -) -> tuple[bool, str]: +) -> tuple[bool, Optional[dict]]: """ Wraps the call in sudo to set hyperparameter values using AdminUtils. @@ -160,10 +159,9 @@ def sudo_set_admin_utils( wallet (Wallet): Wallet object with the keypair for signing. call_function (str): The AdminUtils function to call. call_params (dict): Parameters for the AdminUtils function. - return_error_message (bool): If True, returns the error message along with the success status. Returns: - Union[bool, tuple[bool, Optional[str]]]: Success status or (success status, error message). + tuple[bool, Optional[dict]]: (success status, error details). """ inner_call = substrate.compose_call( call_module="AdminUtils", @@ -185,10 +183,7 @@ def sudo_set_admin_utils( wait_for_finalization=True, ) - if return_error_message: - return response.is_success, response.error_message - - return response.is_success, "" + return response.is_success, response.error_message async def root_set_subtensor_hyperparameter_values( @@ -237,38 +232,6 @@ def set_children(subtensor, wallet, netuid, children): ) -def increase_take(subtensor, wallet, take): - return subtensor.sign_and_send_extrinsic( - subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="increase_take", - call_params={ - "hotkey": wallet.hotkey.ss58_address, - "take": int(take * 0xFFFF), # u16 representation of the take - }, - ), - wallet, - wait_for_inclusion=True, - wait_for_finalization=True, - ) - - -def decrease_take(subtensor, wallet, take): - return subtensor.sign_and_send_extrinsic( - subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="decrease_take", - call_params={ - "hotkey": wallet.hotkey.ss58_address, - "take": int(take * 0xFFFF), # u16 representation of the take - }, - ), - wallet, - wait_for_inclusion=True, - wait_for_finalization=True, - ) - - def set_identity( subtensor, wallet, diff --git a/tests/e2e_tests/utils/e2e_test_utils.py b/tests/e2e_tests/utils/e2e_test_utils.py index edc3f27c72..00e2594b8b 100644 --- a/tests/e2e_tests/utils/e2e_test_utils.py +++ b/tests/e2e_tests/utils/e2e_test_utils.py @@ -1,5 +1,4 @@ import asyncio -import contextlib import os import shutil import subprocess @@ -84,6 +83,107 @@ def uninstall_templates(install_dir): class Templates: + class Miner: + def __init__(self, dir, wallet, netuid): + self.dir = dir + self.wallet = wallet + self.netuid = netuid + self.process = None + + self.started = asyncio.Event() + + async def __aenter__(self): + self.process = await asyncio.create_subprocess_exec( + sys.executable, + f"{self.dir}/miner.py", + "--netuid", + str(self.netuid), + "--subtensor.network", + "local", + "--subtensor.chain_endpoint", + "ws://localhost:9944", + "--wallet.path", + self.wallet.path, + "--wallet.name", + self.wallet.name, + "--wallet.hotkey", + "default", + env={ + "BT_LOGGING_INFO": "1", + }, + stdout=asyncio.subprocess.PIPE, + ) + + self.__reader_task = asyncio.create_task(self._reader()) + + async with asyncio.timeout(30): + await self.started.wait() + + return self + + async def __aexit__(self, exc_type, exc_value, traceback): + self.process.terminate() + self.__reader_task.cancel() + + await self.process.wait() + + async def _reader(self): + async for line in self.process.stdout: + if b"Starting main loop" in line: + self.started.set() + + class Validator: + def __init__(self, dir, wallet, netuid): + self.dir = dir + self.wallet = wallet + self.netuid = netuid + self.process = None + + self.started = asyncio.Event() + self.set_weights = asyncio.Event() + + async def __aenter__(self): + self.process = await asyncio.create_subprocess_exec( + sys.executable, + f"{self.dir}/validator.py", + "--netuid", + str(self.netuid), + "--subtensor.network", + "local", + "--subtensor.chain_endpoint", + "ws://localhost:9944", + "--wallet.path", + self.wallet.path, + "--wallet.name", + self.wallet.name, + "--wallet.hotkey", + "default", + env={ + "BT_LOGGING_INFO": "1", + }, + stdout=asyncio.subprocess.PIPE, + ) + + self.__reader_task = asyncio.create_task(self._reader()) + + async with asyncio.timeout(30): + await self.started.wait() + + return self + + async def __aexit__(self, exc_type, exc_value, traceback): + self.process.terminate() + self.__reader_task.cancel() + + await self.process.wait() + + async def _reader(self): + async for line in self.process.stdout: + if b"Starting validator loop." in line: + self.started.set() + elif b"Successfully set weights and Finalized." in line: + self.set_weights.set() + def __init__(self): self.dir = clone_or_update_templates() @@ -93,52 +193,8 @@ def __enter__(self): def __exit__(self, exc_type, exc_value, traceback): uninstall_templates(self.dir) - @contextlib.asynccontextmanager - async def miner(self, wallet, netuid): - process = await asyncio.create_subprocess_exec( - sys.executable, - f"{self.dir}/miner.py", - "--netuid", - str(netuid), - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9944", - "--wallet.path", - wallet.path, - "--wallet.name", - wallet.name, - "--wallet.hotkey", - "default", - ) - - yield - - process.terminate() - - await process.wait() - - @contextlib.asynccontextmanager - async def validator(self, wallet, netuid): - process = await asyncio.create_subprocess_exec( - sys.executable, - f"{self.dir}/validator.py", - "--netuid", - str(netuid), - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9944", - "--wallet.path", - wallet.path, - "--wallet.name", - wallet.name, - "--wallet.hotkey", - "default", - ) - - yield - - process.terminate() + def miner(self, wallet, netuid): + return self.Miner(self.dir, wallet, netuid) - await process.wait() + def validator(self, wallet, netuid): + return self.Validator(self.dir, wallet, netuid) diff --git a/tests/helpers/helpers.py b/tests/helpers/helpers.py index 70c8342009..f5fda8db6a 100644 --- a/tests/helpers/helpers.py +++ b/tests/helpers/helpers.py @@ -2,7 +2,7 @@ import json import time from collections import deque -from typing import Union +from typing import Optional, Union from bittensor_wallet.mock.wallet_mock import MockWallet as _MockWallet from bittensor_wallet.mock.wallet_mock import get_mock_coldkey @@ -44,6 +44,61 @@ def __eq__(self, __o: Union[float, int, Balance]) -> bool: ) or ((__o - self.tolerance) <= self.value <= (__o + self.tolerance)) +class ApproxBalance(CLOSE_IN_VALUE, Balance): + def __init__( + self, + balance: Union[float, int], + tolerance: Union[float, int] = 0.1, + ): + super().__init__( + Balance(balance), + Balance(tolerance), + ) + + @property + def rao(self): + return self.value.rao + + +def assert_submit_signed_extrinsic( + substrate, + keypair, + call_module, + call_function, + call_params: Optional[dict] = None, + era: Optional[dict] = None, + nonce: Optional[int] = None, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, +): + substrate.compose_call.assert_called_with( + call_module, + call_function, + call_params, + ) + + extrinsic = { + "call": substrate.compose_call.return_value, + "keypair": keypair, + } + + if era: + extrinsic["era"] = era + + if nonce: + extrinsic["nonce"] = nonce + + substrate.create_signed_extrinsic.assert_called_with( + **extrinsic, + ) + + substrate.submit_extrinsic.assert_called_with( + substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + def get_mock_neuron(**kwargs) -> NeuronInfo: """ Returns a mock neuron with the given kwargs overriding the default values. diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index 9933746279..9fb5d19e41 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -1,6 +1,6 @@ import pytest from aioresponses import aioresponses -from async_substrate_interface.sync_substrate import SubstrateInterface +from bittensor_wallet import Wallet import bittensor.core.subtensor @@ -17,21 +17,25 @@ def mock_aio_response(): @pytest.fixture -def mock_substrate_interface(mocker): - mocked = mocker.MagicMock( - autospec=SubstrateInterface, +def mock_substrate(mocker): + mocked = mocker.patch( + "bittensor.core.subtensor.SubstrateInterface", + autospec=True, ) - mocker.patch("bittensor.core.subtensor.SubstrateInterface", return_value=mocked) - - return mocked + return mocked.return_value @pytest.fixture -def subtensor(mock_substrate_interface): +def subtensor(mock_substrate): return bittensor.core.subtensor.Subtensor() +@pytest.fixture +def fake_wallet(mocker): + return mocker.Mock(spec_set=Wallet) + + @pytest.fixture def mock_get_external_ip(mocker): mocked = mocker.Mock( diff --git a/tests/unit_tests/extrinsics/asyncex/conftest.py b/tests/unit_tests/extrinsics/asyncex/conftest.py new file mode 100644 index 0000000000..e2fc3c10b6 --- /dev/null +++ b/tests/unit_tests/extrinsics/asyncex/conftest.py @@ -0,0 +1,18 @@ +import pytest + +import bittensor.core.async_subtensor + + +@pytest.fixture +def mock_substrate(mocker): + mocked = mocker.patch( + "bittensor.core.async_subtensor.AsyncSubstrateInterface", + autospec=True, + ) + + return mocked.return_value + + +@pytest.fixture +def subtensor(mock_substrate): + return bittensor.core.async_subtensor.AsyncSubtensor() diff --git a/tests/unit_tests/extrinsics/asyncex/test_commit_reveal.py b/tests/unit_tests/extrinsics/asyncex/test_commit_reveal.py index fd09bd106b..4dec244651 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_commit_reveal.py +++ b/tests/unit_tests/extrinsics/asyncex/test_commit_reveal.py @@ -1,21 +1,9 @@ from bittensor.core import async_subtensor as subtensor_module from bittensor.core.chain_data import SubnetHyperparameters -from bittensor.core.async_subtensor import AsyncSubtensor from bittensor.core.extrinsics.asyncex import commit_reveal as async_commit_reveal import pytest import torch import numpy as np -from bittensor_wallet import Wallet - - -@pytest.fixture -def subtensor(mocker): - fake_substrate = mocker.AsyncMock() - fake_substrate.websocket.socket.getsockopt.return_value = 0 - mocker.patch.object( - subtensor_module, "AsyncSubstrateInterface", return_value=fake_substrate - ) - yield AsyncSubtensor() @pytest.fixture @@ -52,10 +40,9 @@ def hyperparams(): @pytest.mark.asyncio -async def test_do_commit_reveal_v3_success(mocker, subtensor): +async def test_do_commit_reveal_v3_success(mocker, subtensor, fake_wallet): """Test successful commit-reveal with wait for finalization.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_commit = b"fake_commit" fake_reveal_round = 1 @@ -99,10 +86,9 @@ async def test_do_commit_reveal_v3_success(mocker, subtensor): @pytest.mark.asyncio -async def test_do_commit_reveal_v3_failure_due_to_error(mocker, subtensor): +async def test_do_commit_reveal_v3_failure_due_to_error(mocker, subtensor, fake_wallet): """Test commit-reveal fails due to an error in submission.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_commit = b"fake_commit" fake_reveal_round = 1 @@ -161,11 +147,10 @@ async def test_do_commit_reveal_v3_failure_due_to_error(mocker, subtensor): @pytest.mark.asyncio async def test_commit_reveal_v3_extrinsic_success_with_torch( - mocker, subtensor, hyperparams + mocker, subtensor, hyperparams, fake_wallet ): """Test successful commit-reveal with torch tensors.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_uids = torch.tensor([1, 2, 3], dtype=torch.int64) fake_weights = torch.tensor([0.1, 0.2, 0.7], dtype=torch.float32) @@ -242,11 +227,10 @@ async def test_commit_reveal_v3_extrinsic_success_with_torch( @pytest.mark.asyncio async def test_commit_reveal_v3_extrinsic_success_with_numpy( - mocker, subtensor, hyperparams + mocker, subtensor, hyperparams, fake_wallet ): """Test successful commit-reveal with numpy arrays.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_uids = np.array([1, 2, 3], dtype=np.int64) fake_weights = np.array([0.1, 0.2, 0.7], dtype=np.float32) @@ -290,11 +274,10 @@ async def test_commit_reveal_v3_extrinsic_success_with_numpy( @pytest.mark.asyncio async def test_commit_reveal_v3_extrinsic_response_false( - mocker, subtensor, hyperparams + mocker, subtensor, hyperparams, fake_wallet ): """Test unsuccessful commit-reveal with torch.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_uids = torch.tensor([1, 2, 3], dtype=torch.int64) fake_weights = torch.tensor([0.1, 0.2, 0.7], dtype=torch.float32) @@ -348,10 +331,9 @@ async def test_commit_reveal_v3_extrinsic_response_false( @pytest.mark.asyncio -async def test_commit_reveal_v3_extrinsic_exception(mocker, subtensor): +async def test_commit_reveal_v3_extrinsic_exception(mocker, subtensor, fake_wallet): """Test exception handling in commit-reveal.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_uids = [1, 2, 3] fake_weights = [0.1, 0.2, 0.7] diff --git a/tests/unit_tests/extrinsics/asyncex/test_registration.py b/tests/unit_tests/extrinsics/asyncex/test_registration.py index 5013469d1b..703e1df4cb 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_registration.py +++ b/tests/unit_tests/extrinsics/asyncex/test_registration.py @@ -1,26 +1,13 @@ import pytest -from bittensor_wallet import Wallet from bittensor.core import async_subtensor from bittensor.core.extrinsics.asyncex import registration as async_registration -@pytest.fixture(autouse=True) -def subtensor(mocker): - fake_async_substrate = mocker.AsyncMock( - autospec=async_subtensor.AsyncSubstrateInterface - ) - mocker.patch.object( - async_subtensor, "AsyncSubstrateInterface", return_value=fake_async_substrate - ) - return async_subtensor.AsyncSubtensor() - - @pytest.mark.asyncio -async def test_do_pow_register_success(subtensor, mocker): +async def test_do_pow_register_success(subtensor, fake_wallet, mocker): """Tests successful PoW registration.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_wallet.hotkey.ss58_address = "hotkey_ss58" fake_wallet.coldkeypub.ss58_address = "coldkey_ss58" fake_pow_result = mocker.Mock( @@ -77,10 +64,9 @@ async def test_do_pow_register_success(subtensor, mocker): @pytest.mark.asyncio -async def test_do_pow_register_failure(subtensor, mocker): +async def test_do_pow_register_failure(subtensor, fake_wallet, mocker): """Tests failed PoW registration.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_wallet.hotkey.ss58_address = "hotkey_ss58" fake_wallet.coldkeypub.ss58_address = "coldkey_ss58" fake_pow_result = mocker.Mock( @@ -132,10 +118,9 @@ async def test_do_pow_register_failure(subtensor, mocker): @pytest.mark.asyncio -async def test_do_pow_register_no_waiting(subtensor, mocker): +async def test_do_pow_register_no_waiting(subtensor, fake_wallet, mocker): """Tests PoW registration without waiting for inclusion or finalization.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_wallet.hotkey.ss58_address = "hotkey_ss58" fake_wallet.coldkeypub.ss58_address = "coldkey_ss58" fake_pow_result = mocker.Mock( @@ -177,10 +162,9 @@ async def test_do_pow_register_no_waiting(subtensor, mocker): @pytest.mark.asyncio -async def test_register_extrinsic_success(subtensor, mocker): +async def test_register_extrinsic_success(subtensor, fake_wallet, mocker): """Tests successful registration.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_wallet.hotkey.ss58_address = "hotkey_ss58" fake_wallet.coldkey.ss58_address = "coldkey_ss58" @@ -232,10 +216,9 @@ async def test_register_extrinsic_success(subtensor, mocker): @pytest.mark.asyncio -async def test_register_extrinsic_success_with_cuda(subtensor, mocker): +async def test_register_extrinsic_success_with_cuda(subtensor, fake_wallet, mocker): """Tests successful registration with CUDA enabled.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_wallet.hotkey.ss58_address = "hotkey_ss58" fake_wallet.coldkey.ss58_address = "coldkey_ss58" @@ -289,10 +272,9 @@ async def test_register_extrinsic_success_with_cuda(subtensor, mocker): @pytest.mark.asyncio -async def test_register_extrinsic_failed_with_cuda(subtensor, mocker): +async def test_register_extrinsic_failed_with_cuda(subtensor, fake_wallet, mocker): """Tests failed registration with CUDA enabled.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_wallet.hotkey.ss58_address = "hotkey_ss58" fake_wallet.coldkey.ss58_address = "coldkey_ss58" @@ -330,11 +312,9 @@ async def test_register_extrinsic_failed_with_cuda(subtensor, mocker): @pytest.mark.asyncio -async def test_register_extrinsic_subnet_not_exists(subtensor, mocker): +async def test_register_extrinsic_subnet_not_exists(subtensor, fake_wallet, mocker): """Tests registration when subnet does not exist.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) - mocked_subnet_exists = mocker.patch.object( subtensor, "subnet_exists", return_value=False ) @@ -355,10 +335,9 @@ async def test_register_extrinsic_subnet_not_exists(subtensor, mocker): @pytest.mark.asyncio -async def test_register_extrinsic_already_registered(subtensor, mocker): +async def test_register_extrinsic_already_registered(subtensor, fake_wallet, mocker): """Tests registration when the key is already registered.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) mocked_get_neuron = mocker.patch.object( subtensor, "get_neuron_for_pubkey_and_subnet", @@ -382,9 +361,8 @@ async def test_register_extrinsic_already_registered(subtensor, mocker): @pytest.mark.asyncio -async def test_register_extrinsic_max_attempts_reached(subtensor, mocker): +async def test_register_extrinsic_max_attempts_reached(subtensor, fake_wallet, mocker): # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_wallet.hotkey.ss58_address = "hotkey_ss58" fake_wallet.coldkey.ss58_address = "coldkey_ss58" @@ -450,10 +428,9 @@ async def is_stale_side_effect(*_, **__): @pytest.mark.asyncio -async def test_set_subnet_identity_extrinsic_is_success(subtensor, mocker): +async def test_set_subnet_identity_extrinsic_is_success(subtensor, fake_wallet, mocker): """Verify that set_subnet_identity_extrinsic calls the correct functions and returns the correct result.""" # Preps - wallet = mocker.MagicMock(autospec=Wallet) netuid = 123 subnet_name = "mock_subnet_name" github_repo = "mock_github_repo" @@ -474,7 +451,7 @@ async def test_set_subnet_identity_extrinsic_is_success(subtensor, mocker): # Call result = await async_registration.set_subnet_identity_extrinsic( subtensor=subtensor, - wallet=wallet, + wallet=fake_wallet, netuid=netuid, subnet_name=subnet_name, github_repo=github_repo, @@ -490,7 +467,7 @@ async def test_set_subnet_identity_extrinsic_is_success(subtensor, mocker): call_module="SubtensorModule", call_function="set_subnet_identity", call_params={ - "hotkey": wallet.hotkey.ss58_address, + "hotkey": fake_wallet.hotkey.ss58_address, "netuid": netuid, "subnet_name": subnet_name, "github_repo": github_repo, @@ -503,7 +480,7 @@ async def test_set_subnet_identity_extrinsic_is_success(subtensor, mocker): ) mocked_submit_extrinsic.assert_awaited_once_with( call=mocked_compose_call.return_value, - wallet=wallet, + wallet=fake_wallet, wait_for_inclusion=False, wait_for_finalization=True, ) @@ -512,10 +489,9 @@ async def test_set_subnet_identity_extrinsic_is_success(subtensor, mocker): @pytest.mark.asyncio -async def test_set_subnet_identity_extrinsic_is_failed(subtensor, mocker): +async def test_set_subnet_identity_extrinsic_is_failed(subtensor, fake_wallet, mocker): """Verify that set_subnet_identity_extrinsic calls the correct functions and returns False with bad result.""" # Preps - wallet = mocker.MagicMock(autospec=Wallet) netuid = 123 subnet_name = "mock_subnet_name" github_repo = "mock_github_repo" @@ -538,7 +514,7 @@ async def test_set_subnet_identity_extrinsic_is_failed(subtensor, mocker): # Call result = await async_registration.set_subnet_identity_extrinsic( subtensor=subtensor, - wallet=wallet, + wallet=fake_wallet, netuid=netuid, subnet_name=subnet_name, github_repo=github_repo, @@ -556,7 +532,7 @@ async def test_set_subnet_identity_extrinsic_is_failed(subtensor, mocker): call_module="SubtensorModule", call_function="set_subnet_identity", call_params={ - "hotkey": wallet.hotkey.ss58_address, + "hotkey": fake_wallet.hotkey.ss58_address, "netuid": netuid, "subnet_name": subnet_name, "github_repo": github_repo, @@ -569,7 +545,7 @@ async def test_set_subnet_identity_extrinsic_is_failed(subtensor, mocker): ) mocked_submit_extrinsic.assert_awaited_once_with( call=mocked_compose_call.return_value, - wallet=wallet, + wallet=fake_wallet, wait_for_inclusion=True, wait_for_finalization=True, ) diff --git a/tests/unit_tests/extrinsics/asyncex/test_root.py b/tests/unit_tests/extrinsics/asyncex/test_root.py index c1aed1d6a4..ecca5a9847 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_root.py +++ b/tests/unit_tests/extrinsics/asyncex/test_root.py @@ -1,20 +1,9 @@ import pytest -from bittensor.core import async_subtensor from bittensor.core.errors import SubstrateRequestException from bittensor.core.extrinsics.asyncex import root as async_root -from bittensor_wallet import Wallet - -@pytest.fixture(autouse=True) -def subtensor(mocker): - fake_async_substrate = mocker.AsyncMock( - autospec=async_subtensor.AsyncSubstrateInterface - ) - mocker.patch.object( - async_subtensor, "AsyncSubstrateInterface", return_value=fake_async_substrate - ) - return async_subtensor.AsyncSubtensor() +from bittensor.utils.balance import Balance @pytest.mark.asyncio @@ -47,10 +36,9 @@ async def test_get_limits_success(subtensor, mocker): @pytest.mark.asyncio -async def test_root_register_extrinsic_success(subtensor, mocker): +async def test_root_register_extrinsic_success(subtensor, fake_wallet, mocker): """Tests successful registration to root network.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_wallet.hotkey.ss58_address = "fake_hotkey_address" fake_wallet.hotkey_str = "fake_hotkey" fake_uid = 123 @@ -76,6 +64,16 @@ async def test_root_register_extrinsic_success(subtensor, mocker): "query", return_value=fake_uid, ) + mocker.patch.object( + subtensor, + "get_hyperparameter", + return_value=Balance(0), + ) + mocker.patch.object( + subtensor, + "get_balance", + return_value=Balance(1), + ) # Call result = await async_root.root_register_extrinsic( @@ -101,11 +99,52 @@ async def test_root_register_extrinsic_success(subtensor, mocker): @pytest.mark.asyncio -async def test_root_register_extrinsic_unlock_failed(subtensor, mocker): +async def test_root_register_extrinsic_insufficient_balance( + subtensor, + fake_wallet, + mocker, +): + mocker.patch.object( + subtensor, + "get_hyperparameter", + return_value=Balance(1), + ) + mocker.patch.object( + subtensor, + "get_balance", + return_value=Balance(0), + ) + + result = await async_root.root_register_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + assert result is False + + subtensor.get_balance.assert_called_once_with( + fake_wallet.coldkeypub.ss58_address, + block_hash=subtensor.substrate.get_chain_head.return_value, + ) + subtensor.substrate.submit_extrinsic.assert_not_called() + + +@pytest.mark.asyncio +async def test_root_register_extrinsic_unlock_failed(subtensor, fake_wallet, mocker): """Tests registration fails due to unlock failure.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) - + mocker.patch.object( + subtensor, + "get_hyperparameter", + return_value=Balance(0), + ) + mocker.patch.object( + subtensor, + "get_balance", + return_value=Balance(1), + ) mocked_unlock_key = mocker.patch.object( async_root, "unlock_key", @@ -126,12 +165,23 @@ async def test_root_register_extrinsic_unlock_failed(subtensor, mocker): @pytest.mark.asyncio -async def test_root_register_extrinsic_already_registered(subtensor, mocker): +async def test_root_register_extrinsic_already_registered( + subtensor, fake_wallet, mocker +): """Tests registration when hotkey is already registered.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_wallet.hotkey.ss58_address = "fake_hotkey_address" + mocker.patch.object( + subtensor, + "get_hyperparameter", + return_value=Balance(0), + ) + mocker.patch.object( + subtensor, + "get_balance", + return_value=Balance(1), + ) mocked_unlock_key = mocker.patch.object( async_root, "unlock_key", @@ -160,12 +210,23 @@ async def test_root_register_extrinsic_already_registered(subtensor, mocker): @pytest.mark.asyncio -async def test_root_register_extrinsic_transaction_failed(subtensor, mocker): +async def test_root_register_extrinsic_transaction_failed( + subtensor, fake_wallet, mocker +): """Tests registration fails due to transaction failure.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_wallet.hotkey.ss58_address = "fake_hotkey_address" + mocker.patch.object( + subtensor, + "get_hyperparameter", + return_value=Balance(0), + ) + mocker.patch.object( + subtensor, + "get_balance", + return_value=Balance(1), + ) mocked_unlock_key = mocker.patch.object( async_root, "unlock_key", @@ -202,12 +263,21 @@ async def test_root_register_extrinsic_transaction_failed(subtensor, mocker): @pytest.mark.asyncio -async def test_root_register_extrinsic_uid_not_found(subtensor, mocker): +async def test_root_register_extrinsic_uid_not_found(subtensor, fake_wallet, mocker): """Tests registration fails because UID is not found after successful transaction.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_wallet.hotkey.ss58_address = "fake_hotkey_address" + mocker.patch.object( + subtensor, + "get_hyperparameter", + return_value=Balance(0), + ) + mocker.patch.object( + subtensor, + "get_balance", + return_value=Balance(1), + ) mocked_unlock_key = mocker.patch.object( async_root, "unlock_key", @@ -254,10 +324,9 @@ async def test_root_register_extrinsic_uid_not_found(subtensor, mocker): @pytest.mark.asyncio -async def test_do_set_root_weights_success(subtensor, mocker): +async def test_do_set_root_weights_success(subtensor, fake_wallet, mocker): """Tests _do_set_root_weights when weights are set successfully.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_wallet.hotkey.ss58_address = "fake_hotkey_address" fake_uids = [1, 2, 3] fake_weights = [0.1, 0.2, 0.7] @@ -314,10 +383,9 @@ async def test_do_set_root_weights_success(subtensor, mocker): @pytest.mark.asyncio -async def test_do_set_root_weights_failure(subtensor, mocker): +async def test_do_set_root_weights_failure(subtensor, fake_wallet, mocker): """Tests _do_set_root_weights when setting weights fails.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_wallet.hotkey.ss58_address = "fake_hotkey_address" fake_uids = [1, 2, 3] fake_weights = [0.1, 0.2, 0.7] @@ -364,10 +432,9 @@ async def fake_is_success(): @pytest.mark.asyncio -async def test_do_set_root_weights_no_waiting(subtensor, mocker): +async def test_do_set_root_weights_no_waiting(subtensor, fake_wallet, mocker): """Tests _do_set_root_weights when not waiting for inclusion or finalization.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_wallet.hotkey.ss58_address = "fake_hotkey_address" fake_uids = [1, 2, 3] fake_weights = [0.1, 0.2, 0.7] @@ -406,9 +473,8 @@ async def test_do_set_root_weights_no_waiting(subtensor, mocker): @pytest.mark.asyncio -async def test_set_root_weights_extrinsic_success(subtensor, mocker): +async def test_set_root_weights_extrinsic_success(subtensor, fake_wallet, mocker): """Tests successful setting of root weights.""" - fake_wallet = mocker.Mock(autospec=Wallet) fake_wallet.hotkey.ss58_address = "fake_hotkey" netuids = [1, 2, 3] weights = [0.1, 0.2, 0.7] @@ -439,9 +505,8 @@ async def test_set_root_weights_extrinsic_success(subtensor, mocker): @pytest.mark.asyncio -async def test_set_root_weights_extrinsic_no_waiting(subtensor, mocker): +async def test_set_root_weights_extrinsic_no_waiting(subtensor, fake_wallet, mocker): """Tests setting root weights without waiting for inclusion or finalization.""" - fake_wallet = mocker.Mock(autospec=Wallet) fake_wallet.hotkey.ss58_address = "fake_hotkey" netuids = [1, 2, 3] weights = [0.1, 0.2, 0.7] @@ -472,9 +537,10 @@ async def test_set_root_weights_extrinsic_no_waiting(subtensor, mocker): @pytest.mark.asyncio -async def test_set_root_weights_extrinsic_not_registered(subtensor, mocker): +async def test_set_root_weights_extrinsic_not_registered( + subtensor, fake_wallet, mocker +): """Tests failure when hotkey is not registered.""" - fake_wallet = mocker.Mock(autospec=Wallet) fake_wallet.hotkey.ss58_address = "fake_hotkey" mocker.patch.object(subtensor.substrate, "query", return_value=None) @@ -490,9 +556,10 @@ async def test_set_root_weights_extrinsic_not_registered(subtensor, mocker): @pytest.mark.asyncio -async def test_set_root_weights_extrinsic_insufficient_weights(subtensor, mocker): +async def test_set_root_weights_extrinsic_insufficient_weights( + subtensor, fake_wallet, mocker +): """Tests failure when number of weights is less than the minimum allowed.""" - fake_wallet = mocker.Mock(autospec=Wallet) fake_wallet.hotkey.ss58_address = "fake_hotkey" netuids = [1, 2] weights = [0.5, 0.5] @@ -513,9 +580,8 @@ async def test_set_root_weights_extrinsic_insufficient_weights(subtensor, mocker @pytest.mark.asyncio -async def test_set_root_weights_extrinsic_unlock_failed(subtensor, mocker): +async def test_set_root_weights_extrinsic_unlock_failed(subtensor, fake_wallet, mocker): """Tests failure due to unlock key error.""" - fake_wallet = mocker.Mock(autospec=Wallet) fake_wallet.hotkey.ss58_address = "fake_hotkey" mocker.patch.object(subtensor.substrate, "query", return_value=123) @@ -536,9 +602,10 @@ async def test_set_root_weights_extrinsic_unlock_failed(subtensor, mocker): @pytest.mark.asyncio -async def test_set_root_weights_extrinsic_transaction_failed(subtensor, mocker): +async def test_set_root_weights_extrinsic_transaction_failed( + subtensor, fake_wallet, mocker +): """Tests failure when transaction is not successful.""" - fake_wallet = mocker.Mock(autospec=Wallet) fake_wallet.hotkey.ss58_address = "fake_hotkey" mocker.patch.object(subtensor.substrate, "query", return_value=123) @@ -569,9 +636,10 @@ async def test_set_root_weights_extrinsic_transaction_failed(subtensor, mocker): @pytest.mark.asyncio -async def test_set_root_weights_extrinsic_request_exception(subtensor, mocker): +async def test_set_root_weights_extrinsic_request_exception( + subtensor, fake_wallet, mocker +): """Tests failure due to SubstrateRequestException.""" - fake_wallet = mocker.Mock(autospec=Wallet) fake_wallet.hotkey.ss58_address = "fake_hotkey" mocker.patch.object(subtensor.substrate, "query", return_value=123) diff --git a/tests/unit_tests/extrinsics/asyncex/test_transfer.py b/tests/unit_tests/extrinsics/asyncex/test_transfer.py index 0d15d7b577..0fa70a8e75 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_transfer.py +++ b/tests/unit_tests/extrinsics/asyncex/test_transfer.py @@ -1,26 +1,12 @@ import pytest -from bittensor.core import async_subtensor -from bittensor_wallet import Wallet from bittensor.core.extrinsics.asyncex import transfer as async_transfer from bittensor.utils.balance import Balance -@pytest.fixture(autouse=True) -def subtensor(mocker): - fake_async_substrate = mocker.AsyncMock( - autospec=async_subtensor.AsyncSubstrateInterface - ) - mocker.patch.object( - async_subtensor, "AsyncSubstrateInterface", return_value=fake_async_substrate - ) - return async_subtensor.AsyncSubtensor() - - @pytest.mark.asyncio -async def test_do_transfer_success(subtensor, mocker): +async def test_do_transfer_success(subtensor, fake_wallet, mocker): """Tests _do_transfer when the transfer is successful.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_destination = "destination_address" fake_amount = mocker.Mock(autospec=Balance, rao=1000) @@ -70,10 +56,9 @@ async def test_do_transfer_success(subtensor, mocker): @pytest.mark.asyncio -async def test_do_transfer_failure(subtensor, mocker): +async def test_do_transfer_failure(subtensor, fake_wallet, mocker): """Tests _do_transfer when the transfer fails.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_destination = "destination_address" fake_amount = mocker.Mock(autospec=Balance, rao=1000) @@ -130,10 +115,9 @@ async def test_do_transfer_failure(subtensor, mocker): @pytest.mark.asyncio -async def test_do_transfer_no_waiting(subtensor, mocker): +async def test_do_transfer_no_waiting(subtensor, fake_wallet, mocker): """Tests _do_transfer when no waiting for inclusion or finalization.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_destination = "destination_address" fake_amount = mocker.Mock(autospec=Balance, rao=1000) @@ -180,10 +164,9 @@ async def test_do_transfer_no_waiting(subtensor, mocker): @pytest.mark.asyncio -async def test_transfer_extrinsic_success(subtensor, mocker): +async def test_transfer_extrinsic_success(subtensor, fake_wallet, mocker): """Tests successful transfer.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_wallet.coldkeypub.ss58_address = "fake_ss58_address" fake_destination = "valid_ss58_address" fake_amount = Balance(15) @@ -244,11 +227,10 @@ async def test_transfer_extrinsic_success(subtensor, mocker): @pytest.mark.asyncio async def test_transfer_extrinsic_call_successful_with_failed_response( - subtensor, mocker + subtensor, fake_wallet, mocker ): """Tests successful transfer call is successful with failed response.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_wallet.coldkeypub.ss58_address = "fake_ss58_address" fake_destination = "valid_ss58_address" fake_amount = Balance(15) @@ -309,10 +291,9 @@ async def test_transfer_extrinsic_call_successful_with_failed_response( @pytest.mark.asyncio -async def test_transfer_extrinsic_insufficient_balance(subtensor, mocker): +async def test_transfer_extrinsic_insufficient_balance(subtensor, fake_wallet, mocker): """Tests transfer when balance is insufficient.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_wallet.coldkeypub.ss58_address = "fake_ss58_address" fake_destination = "valid_ss58_address" fake_amount = Balance(5000) @@ -366,10 +347,9 @@ async def test_transfer_extrinsic_insufficient_balance(subtensor, mocker): @pytest.mark.asyncio -async def test_transfer_extrinsic_invalid_destination(subtensor, mocker): +async def test_transfer_extrinsic_invalid_destination(subtensor, fake_wallet, mocker): """Tests transfer with invalid destination address.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_wallet.coldkeypub.ss58_address = "fake_ss58_address" fake_destination = "invalid_address" fake_amount = Balance(15) @@ -398,10 +378,9 @@ async def test_transfer_extrinsic_invalid_destination(subtensor, mocker): @pytest.mark.asyncio -async def test_transfer_extrinsic_unlock_key_false(subtensor, mocker): +async def test_transfer_extrinsic_unlock_key_false(subtensor, fake_wallet, mocker): """Tests transfer failed unlock_key.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_wallet.coldkeypub.ss58_address = "fake_ss58_address" fake_destination = "invalid_address" fake_amount = Balance(15) @@ -438,11 +417,10 @@ async def test_transfer_extrinsic_unlock_key_false(subtensor, mocker): @pytest.mark.asyncio async def test_transfer_extrinsic_keep_alive_false_and_transfer_all_true( - subtensor, mocker + subtensor, fake_wallet, mocker ): """Tests transfer with keep_alive flag set to False and transfer_all flag set to True.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_wallet.coldkeypub.ss58_address = "fake_ss58_address" fake_destination = "valid_ss58_address" fake_amount = Balance(15) @@ -460,7 +438,7 @@ async def test_transfer_extrinsic_keep_alive_false_and_transfer_all_true( mocked_get_chain_head = mocker.patch.object( subtensor.substrate, "get_chain_head", return_value="some_block_hash" ) - mocked_get_balance = mocker.patch.object( + mocker.patch.object( subtensor, "get_balance", return_value=1, diff --git a/tests/unit_tests/extrinsics/asyncex/test_weights.py b/tests/unit_tests/extrinsics/asyncex/test_weights.py index b654431429..8a297602f1 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_weights.py +++ b/tests/unit_tests/extrinsics/asyncex/test_weights.py @@ -1,25 +1,12 @@ import pytest from bittensor.core import async_subtensor -from bittensor_wallet import Wallet from bittensor.core.extrinsics.asyncex import weights as async_weights -@pytest.fixture(autouse=True) -def subtensor(mocker): - fake_async_substrate = mocker.AsyncMock( - autospec=async_subtensor.AsyncSubstrateInterface - ) - mocker.patch.object( - async_subtensor, "AsyncSubstrateInterface", return_value=fake_async_substrate - ) - return async_subtensor.AsyncSubtensor() - - @pytest.mark.asyncio -async def test_do_set_weights_success(subtensor, mocker): +async def test_do_set_weights_success(subtensor, fake_wallet, mocker): """Tests _do_set_weights when weights are set successfully.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_uids = [1, 2, 3] fake_vals = [100, 200, 300] fake_netuid = 0 @@ -56,14 +43,13 @@ async def fake_is_success(): # Asserts assert result is True - assert message is "" + assert message == "" @pytest.mark.asyncio -async def test_do_set_weights_failure(subtensor, mocker): +async def test_do_set_weights_failure(subtensor, fake_wallet, mocker): """Tests _do_set_weights when setting weights fails.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_uids = [1, 2, 3] fake_vals = [100, 200, 300] fake_netuid = 0 @@ -113,10 +99,9 @@ async def fake_is_success(): @pytest.mark.asyncio -async def test_do_set_weights_no_waiting(subtensor, mocker): +async def test_do_set_weights_no_waiting(subtensor, fake_wallet, mocker): """Tests _do_set_weights when not waiting for inclusion or finalization.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_uids = [1, 2, 3] fake_vals = [100, 200, 300] fake_netuid = 0 @@ -146,14 +131,15 @@ async def test_do_set_weights_no_waiting(subtensor, mocker): # Asserts assert result is True - assert message is "" + assert message == "" @pytest.mark.asyncio -async def test_set_weights_extrinsic_success_with_finalization(subtensor, mocker): +async def test_set_weights_extrinsic_success_with_finalization( + subtensor, fake_wallet, mocker +): """Tests set_weights_extrinsic when weights are successfully set with finalization.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_uids = [1, 2, 3] fake_weights = [0.1, 0.2, 0.7] @@ -189,10 +175,9 @@ async def test_set_weights_extrinsic_success_with_finalization(subtensor, mocker @pytest.mark.asyncio -async def test_set_weights_extrinsic_no_waiting(subtensor, mocker): +async def test_set_weights_extrinsic_no_waiting(subtensor, fake_wallet, mocker): """Tests set_weights_extrinsic when no waiting for inclusion or finalization.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_uids = [1, 2, 3] fake_weights = [0.1, 0.2, 0.7] @@ -219,10 +204,9 @@ async def test_set_weights_extrinsic_no_waiting(subtensor, mocker): @pytest.mark.asyncio -async def test_set_weights_extrinsic_failure(subtensor, mocker): +async def test_set_weights_extrinsic_failure(subtensor, fake_wallet, mocker): """Tests set_weights_extrinsic when setting weights fails.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_uids = [1, 2, 3] fake_weights = [0.1, 0.2, 0.7] @@ -249,10 +233,9 @@ async def test_set_weights_extrinsic_failure(subtensor, mocker): @pytest.mark.asyncio -async def test_set_weights_extrinsic_exception(subtensor, mocker): +async def test_set_weights_extrinsic_exception(subtensor, fake_wallet, mocker): """Tests set_weights_extrinsic when an exception is raised.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_uids = [1, 2, 3] fake_weights = [0.1, 0.2, 0.7] @@ -279,10 +262,9 @@ async def test_set_weights_extrinsic_exception(subtensor, mocker): @pytest.mark.asyncio -async def test_do_commit_weights_success(subtensor, mocker): +async def test_do_commit_weights_success(subtensor, fake_wallet, mocker): """Tests _do_commit_weights when the commit is successful.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_commit_hash = "test_hash" @@ -316,14 +298,13 @@ async def fake_is_success(): # Asserts assert result is True - assert message is "" + assert message == "" @pytest.mark.asyncio -async def test_do_commit_weights_failure(subtensor, mocker): +async def test_do_commit_weights_failure(subtensor, fake_wallet, mocker): """Tests _do_commit_weights when the commit fails.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_commit_hash = "test_hash" @@ -368,10 +349,9 @@ async def fake_is_success(): @pytest.mark.asyncio -async def test_do_commit_weights_no_waiting(subtensor, mocker): +async def test_do_commit_weights_no_waiting(subtensor, fake_wallet, mocker): """Tests _do_commit_weights when not waiting for inclusion or finalization.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_commit_hash = "test_hash" @@ -399,14 +379,13 @@ async def test_do_commit_weights_no_waiting(subtensor, mocker): # Asserts assert result is True - assert message is "" + assert message == "" @pytest.mark.asyncio -async def test_do_commit_weights_exception(subtensor, mocker): +async def test_do_commit_weights_exception(subtensor, fake_wallet, mocker): """Tests _do_commit_weights when an exception is raised.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_commit_hash = "test_hash" @@ -429,10 +408,9 @@ async def test_do_commit_weights_exception(subtensor, mocker): @pytest.mark.asyncio -async def test_commit_weights_extrinsic_success(subtensor, mocker): +async def test_commit_weights_extrinsic_success(subtensor, fake_wallet, mocker): """Tests commit_weights_extrinsic when the commit is successful.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_commit_hash = "test_hash" @@ -464,10 +442,9 @@ async def test_commit_weights_extrinsic_success(subtensor, mocker): @pytest.mark.asyncio -async def test_commit_weights_extrinsic_failure(subtensor, mocker): +async def test_commit_weights_extrinsic_failure(subtensor, fake_wallet, mocker): """Tests commit_weights_extrinsic when the commit fails.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_commit_hash = "test_hash" diff --git a/tests/unit_tests/extrinsics/test_commit_reveal.py b/tests/unit_tests/extrinsics/test_commit_reveal.py index 33825dfd53..406bd0a824 100644 --- a/tests/unit_tests/extrinsics/test_commit_reveal.py +++ b/tests/unit_tests/extrinsics/test_commit_reveal.py @@ -1,7 +1,6 @@ import numpy as np import pytest import torch -from bittensor_wallet import Wallet from bittensor.core import subtensor as subtensor_module from bittensor.core.chain_data import SubnetHyperparameters @@ -41,10 +40,9 @@ def hyperparams(): ) -def test_do_commit_reveal_v3_success(mocker, subtensor): +def test_do_commit_reveal_v3_success(mocker, subtensor, fake_wallet): """Test successful commit-reveal with wait for finalization.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_commit = b"fake_commit" fake_reveal_round = 1 @@ -87,10 +85,9 @@ def test_do_commit_reveal_v3_success(mocker, subtensor): assert result == (True, "") -def test_do_commit_reveal_v3_failure_due_to_error(mocker, subtensor): +def test_do_commit_reveal_v3_failure_due_to_error(mocker, subtensor, fake_wallet): """Test commit-reveal fails due to an error in submission.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_commit = b"fake_commit" fake_reveal_round = 1 @@ -141,10 +138,11 @@ def test_do_commit_reveal_v3_failure_due_to_error(mocker, subtensor): assert result == (False, "Formatted error") -def test_commit_reveal_v3_extrinsic_success_with_torch(mocker, subtensor, hyperparams): +def test_commit_reveal_v3_extrinsic_success_with_torch( + mocker, subtensor, hyperparams, fake_wallet +): """Test successful commit-reveal with torch tensors.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_uids = torch.tensor([1, 2, 3], dtype=torch.int64) fake_weights = torch.tensor([0.1, 0.2, 0.7], dtype=torch.float32) @@ -160,9 +158,7 @@ def test_commit_reveal_v3_extrinsic_success_with_torch(mocker, subtensor, hyperp "convert_weights_and_uids_for_emit", return_value=(mocked_uids, mocked_weights), ) - mocked_get_subnet_reveal_period_epochs = mocker.patch.object( - subtensor, "get_subnet_reveal_period_epochs" - ) + mocker.patch.object(subtensor, "get_subnet_reveal_period_epochs") mocked_get_encrypted_commit = mocker.patch.object( commit_reveal, "get_encrypted_commit", @@ -215,10 +211,11 @@ def test_commit_reveal_v3_extrinsic_success_with_torch(mocker, subtensor, hyperp ) -def test_commit_reveal_v3_extrinsic_success_with_numpy(mocker, subtensor, hyperparams): +def test_commit_reveal_v3_extrinsic_success_with_numpy( + mocker, subtensor, hyperparams, fake_wallet +): """Test successful commit-reveal with numpy arrays.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_uids = np.array([1, 2, 3], dtype=np.int64) fake_weights = np.array([0.1, 0.2, 0.7], dtype=np.float32) @@ -260,10 +257,11 @@ def test_commit_reveal_v3_extrinsic_success_with_numpy(mocker, subtensor, hyperp mock_do_commit.assert_called_once() -def test_commit_reveal_v3_extrinsic_response_false(mocker, subtensor, hyperparams): +def test_commit_reveal_v3_extrinsic_response_false( + mocker, subtensor, hyperparams, fake_wallet +): """Test unsuccessful commit-reveal with torch.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_uids = torch.tensor([1, 2, 3], dtype=torch.int64) fake_weights = torch.tensor([0.1, 0.2, 0.7], dtype=torch.float32) @@ -316,10 +314,9 @@ def test_commit_reveal_v3_extrinsic_response_false(mocker, subtensor, hyperparam ) -def test_commit_reveal_v3_extrinsic_exception(mocker, subtensor): +def test_commit_reveal_v3_extrinsic_exception(mocker, subtensor, fake_wallet): """Test exception handling in commit-reveal.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_uids = [1, 2, 3] fake_weights = [0.1, 0.2, 0.7] diff --git a/tests/unit_tests/extrinsics/test_commit_weights.py b/tests/unit_tests/extrinsics/test_commit_weights.py index 14993562a2..71771d2247 100644 --- a/tests/unit_tests/extrinsics/test_commit_weights.py +++ b/tests/unit_tests/extrinsics/test_commit_weights.py @@ -1,5 +1,3 @@ -from bittensor_wallet import Wallet - from bittensor.core.extrinsics.commit_weights import ( _do_commit_weights, _do_reveal_weights, @@ -7,10 +5,9 @@ from bittensor.core.settings import version_as_int -def test_do_commit_weights(subtensor, mocker): +def test_do_commit_weights(subtensor, fake_wallet, mocker): """Successful _do_commit_weights call.""" # Preps - fake_wallet = mocker.MagicMock() netuid = 1 commit_hash = "fake_commit_hash" wait_for_inclusion = True @@ -65,10 +62,9 @@ def test_do_commit_weights(subtensor, mocker): ) -def test_do_reveal_weights(subtensor, mocker): +def test_do_reveal_weights(subtensor, fake_wallet, mocker): """Verifies that the `_do_reveal_weights` method interacts with the right substrate methods.""" # Preps - fake_wallet = mocker.MagicMock(autospec=Wallet) fake_wallet.hotkey.ss58_address = "hotkey" netuid = 1 diff --git a/tests/unit_tests/extrinsics/test_root.py b/tests/unit_tests/extrinsics/test_root.py index 34788a2768..a22d456a2e 100644 --- a/tests/unit_tests/extrinsics/test_root.py +++ b/tests/unit_tests/extrinsics/test_root.py @@ -1,6 +1,7 @@ import pytest from bittensor.core.subtensor import Subtensor from bittensor.core.extrinsics import root +from bittensor.utils.balance import Balance @pytest.fixture @@ -75,6 +76,11 @@ def test_root_register_extrinsic( "query", return_value=hotkey_registered[1], ) + mocker.patch.object( + mock_subtensor, + "get_balance", + return_value=Balance(1), + ) # Act result = root.root_register_extrinsic( @@ -100,6 +106,31 @@ def test_root_register_extrinsic( ) +def test_root_register_extrinsic_insufficient_balance( + mock_subtensor, + mock_wallet, + mocker, +): + mocker.patch.object( + mock_subtensor, + "get_balance", + return_value=Balance(0), + ) + + success = root.root_register_extrinsic( + subtensor=mock_subtensor, + wallet=mock_wallet, + ) + + assert success is False + + mock_subtensor.get_balance.assert_called_once_with( + mock_wallet.coldkeypub.ss58_address, + block=mock_subtensor.get_current_block.return_value, + ) + mock_subtensor.substrate.submit_extrinsic.assert_not_called() + + @pytest.mark.parametrize( "wait_for_inclusion, wait_for_finalization, netuids, weights, expected_success", [ diff --git a/tests/unit_tests/extrinsics/test_set_weights.py b/tests/unit_tests/extrinsics/test_set_weights.py index a2aaa4aaab..fdded8d442 100644 --- a/tests/unit_tests/extrinsics/test_set_weights.py +++ b/tests/unit_tests/extrinsics/test_set_weights.py @@ -2,7 +2,6 @@ import pytest import torch -from bittensor_wallet import Wallet from bittensor.core import subtensor as subtensor_module from bittensor.core.extrinsics.set_weights import ( @@ -21,12 +20,6 @@ def mock_subtensor(): return mock -@pytest.fixture -def mock_wallet(): - mock = MagicMock(spec=Wallet) - return mock - - @pytest.mark.parametrize( "uids, weights, version_key, wait_for_inclusion, wait_for_finalization, expected_success, expected_message", [ @@ -66,7 +59,7 @@ def mock_wallet(): ) def test_set_weights_extrinsic( mock_subtensor, - mock_wallet, + fake_wallet, uids, weights, version_key, @@ -86,7 +79,7 @@ def test_set_weights_extrinsic( ): result, message = set_weights_extrinsic( subtensor=mock_subtensor, - wallet=mock_wallet, + wallet=fake_wallet, netuid=123, uids=uids, weights=weights, @@ -99,10 +92,9 @@ def test_set_weights_extrinsic( assert message == expected_message, f"Test {expected_message} failed." -def test_do_set_weights_is_success(mock_subtensor, mocker): +def test_do_set_weights_is_success(mock_subtensor, fake_wallet, mocker): """Successful _do_set_weights call.""" # Prep - fake_wallet = mocker.MagicMock() fake_uids = [1, 2, 3] fake_vals = [4, 5, 6] fake_netuid = 1 @@ -144,10 +136,9 @@ def test_do_set_weights_is_success(mock_subtensor, mocker): assert result == (True, "Successfully set weights.") -def test_do_set_weights_is_not_success(mock_subtensor, mocker): +def test_do_set_weights_is_not_success(mock_subtensor, fake_wallet, mocker): """Unsuccessful _do_set_weights call.""" # Prep - fake_wallet = mocker.MagicMock() fake_uids = [1, 2, 3] fake_vals = [4, 5, 6] fake_netuid = 1 @@ -200,10 +191,9 @@ def test_do_set_weights_is_not_success(mock_subtensor, mocker): ) -def test_do_set_weights_no_waits(mock_subtensor, mocker): +def test_do_set_weights_no_waits(mock_subtensor, fake_wallet, mocker): """Successful _do_set_weights call without wait flags for fake_wait_for_inclusion and fake_wait_for_finalization.""" # Prep - fake_wallet = mocker.MagicMock() fake_uids = [1, 2, 3] fake_vals = [4, 5, 6] fake_netuid = 1 diff --git a/tests/unit_tests/extrinsics/test_staking.py b/tests/unit_tests/extrinsics/test_staking.py index 9cdd241c89..899054006f 100644 --- a/tests/unit_tests/extrinsics/test_staking.py +++ b/tests/unit_tests/extrinsics/test_staking.py @@ -1,5 +1,4 @@ from bittensor.core.extrinsics import staking -from bittensor.core.extrinsics import utils from bittensor.utils.balance import Balance diff --git a/tests/unit_tests/extrinsics/test_transfer.py b/tests/unit_tests/extrinsics/test_transfer.py index 95f1cf8de5..9424352a55 100644 --- a/tests/unit_tests/extrinsics/test_transfer.py +++ b/tests/unit_tests/extrinsics/test_transfer.py @@ -2,10 +2,9 @@ from bittensor.utils.balance import Balance -def test_do_transfer_is_success_true(subtensor, mocker): +def test_do_transfer_is_success_true(subtensor, fake_wallet, mocker): """Successful do_transfer call.""" # Prep - fake_wallet = mocker.MagicMock() fake_dest = "SS58PUBLICKEY" fake_transfer_balance = Balance(1) fake_wait_for_inclusion = True @@ -45,10 +44,9 @@ def test_do_transfer_is_success_true(subtensor, mocker): ) -def test_do_transfer_is_success_false(subtensor, mocker): +def test_do_transfer_is_success_false(subtensor, fake_wallet, mocker): """Successful do_transfer call.""" # Prep - fake_wallet = mocker.MagicMock() fake_dest = "SS58PUBLICKEY" fake_transfer_balance = Balance(1) fake_wait_for_inclusion = True @@ -97,10 +95,9 @@ def test_do_transfer_is_success_false(subtensor, mocker): ) -def test_do_transfer_no_waits(subtensor, mocker): +def test_do_transfer_no_waits(subtensor, fake_wallet, mocker): """Successful do_transfer call.""" # Prep - fake_wallet = mocker.MagicMock() fake_dest = "SS58PUBLICKEY" fake_transfer_balance = Balance(1) fake_wait_for_inclusion = False diff --git a/tests/unit_tests/extrinsics/test_unstaking.py b/tests/unit_tests/extrinsics/test_unstaking.py index b93f335162..224fe4b640 100644 --- a/tests/unit_tests/extrinsics/test_unstaking.py +++ b/tests/unit_tests/extrinsics/test_unstaking.py @@ -2,7 +2,7 @@ from bittensor.utils.balance import Balance -def test_unstake_extrinsic(mocker): +def test_unstake_extrinsic(fake_wallet, mocker): # Preps fake_subtensor = mocker.Mock( **{ @@ -12,7 +12,6 @@ def test_unstake_extrinsic(mocker): "get_stake.return_value": Balance(10.0), } ) - fake_wallet = mocker.Mock() fake_wallet.coldkeypub.ss58_address = "hotkey_owner" hotkey_ss58 = "hotkey" fake_netuid = 1 @@ -54,7 +53,7 @@ def test_unstake_extrinsic(mocker): ) -def test_unstake_multiple_extrinsic(mocker): +def test_unstake_multiple_extrinsic(fake_wallet, mocker): """Verify that sync `unstake_multiple_extrinsic` method calls proper async method.""" # Preps fake_subtensor = mocker.Mock( @@ -68,7 +67,6 @@ def test_unstake_multiple_extrinsic(mocker): mocker.patch.object( unstaking, "get_old_stakes", return_value=[Balance(1.1), Balance(0.3)] ) - fake_wallet = mocker.Mock() fake_wallet.coldkeypub.ss58_address = "hotkey_owner" hotkey_ss58s = ["hotkey1", "hotkey2"] fake_netuids = [1, 2] diff --git a/tests/unit_tests/extrinsics/test_utils.py b/tests/unit_tests/extrinsics/test_utils.py index ab6d53f82f..060f45146b 100644 --- a/tests/unit_tests/extrinsics/test_utils.py +++ b/tests/unit_tests/extrinsics/test_utils.py @@ -1,7 +1,6 @@ from unittest.mock import MagicMock import pytest -from async_substrate_interface import SubstrateInterface from scalecodec.types import GenericExtrinsic from bittensor.core.extrinsics import utils @@ -9,9 +8,8 @@ @pytest.fixture -def mock_subtensor(): +def mock_subtensor(mock_substrate): mock_subtensor = MagicMock(autospec=Subtensor) - mock_substrate = MagicMock(autospec=SubstrateInterface) mock_subtensor.substrate = mock_substrate yield mock_subtensor diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index be95739324..3ddbf14f99 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -9,19 +9,26 @@ from bittensor.core import async_subtensor from bittensor.core.async_subtensor import AsyncSubtensor from bittensor.core.chain_data.chain_identity import ChainIdentity +from bittensor.core.chain_data.neuron_info import NeuronInfo from bittensor.core.chain_data.stake_info import StakeInfo from bittensor.core.chain_data import proposal_vote_data from bittensor.utils.balance import Balance +from tests.helpers.helpers import assert_submit_signed_extrinsic -@pytest.fixture(autouse=True) -def subtensor(mocker): - fake_async_substrate = mocker.AsyncMock( - autospec=async_subtensor.AsyncSubstrateInterface - ) - mocker.patch.object( - async_subtensor, "AsyncSubstrateInterface", return_value=fake_async_substrate +@pytest.fixture +def mock_substrate(mocker): + mocked = mocker.patch( + "bittensor.core.async_subtensor.AsyncSubstrateInterface", + autospec=True, ) + mocked.return_value.get_block_hash = mocker.AsyncMock() + + return mocked.return_value + + +@pytest.fixture +def subtensor(mock_substrate): return async_subtensor.AsyncSubtensor() @@ -62,11 +69,10 @@ def test_decode_hex_identity_dict_with_non_tuple_value(): @pytest.mark.asyncio -async def test_init_if_unknown_network_is_valid(mocker): +async def test_init_if_unknown_network_is_valid(mock_substrate): """Tests __init__ if passed network unknown and is valid.""" # Preps fake_valid_endpoint = "wss://blabla.net" - mocker.patch.object(async_subtensor, "AsyncSubstrateInterface") # Call subtensor = AsyncSubtensor(fake_valid_endpoint) @@ -77,11 +83,10 @@ async def test_init_if_unknown_network_is_valid(mocker): @pytest.mark.asyncio -async def test_init_if_unknown_network_is_known_endpoint(mocker): +async def test_init_if_unknown_network_is_known_endpoint(mock_substrate): """Tests __init__ if passed network unknown and is valid.""" # Preps fake_valid_endpoint = "ws://127.0.0.1:9944" - mocker.patch.object(async_subtensor, "AsyncSubstrateInterface") # Call subtensor = AsyncSubtensor(fake_valid_endpoint) @@ -92,10 +97,8 @@ async def test_init_if_unknown_network_is_known_endpoint(mocker): @pytest.mark.asyncio -async def test_init_if_unknown_network_is_not_valid(mocker): +async def test_init_if_unknown_network_is_not_valid(mock_substrate): """Tests __init__ if passed network unknown and isn't valid.""" - # Preps - mocker.patch.object(async_subtensor, "AsyncSubstrateInterface") # Call subtensor = AsyncSubtensor("blabla-net") @@ -115,15 +118,8 @@ def test__str__return(subtensor): @pytest.mark.asyncio -async def test_async_subtensor_magic_methods(mocker): +async def test_async_subtensor_magic_methods(mock_substrate): """Tests async magic methods of AsyncSubtensor class.""" - # Preps - fake_async_substrate = mocker.AsyncMock( - autospec=async_subtensor.AsyncSubstrateInterface - ) - mocker.patch.object( - async_subtensor, "AsyncSubstrateInterface", return_value=fake_async_substrate - ) # Call subtensor = async_subtensor.AsyncSubtensor(network="local") @@ -131,8 +127,8 @@ async def test_async_subtensor_magic_methods(mocker): pass # Asserts - fake_async_substrate.initialize.assert_called_once() - fake_async_substrate.close.assert_called_once() + mock_substrate.initialize.assert_called_once() + mock_substrate.close.assert_called_once() @pytest.mark.parametrize( @@ -163,6 +159,90 @@ async def test_async_subtensor_aenter_connection_refused_error( fake_async_substrate.initialize.assert_awaited_once() +@pytest.mark.asyncio +async def test_burned_register(mock_substrate, subtensor, fake_wallet, mocker): + mock_substrate.submit_extrinsic.return_value = mocker.AsyncMock( + is_success=mocker.AsyncMock(return_value=True)(), + ) + mocker.patch.object( + subtensor, + "get_neuron_for_pubkey_and_subnet", + return_value=NeuronInfo.get_null_neuron(), + ) + mocker.patch.object( + subtensor, + "get_balance", + return_value=Balance(1), + ) + + success = await subtensor.burned_register( + fake_wallet, + netuid=1, + ) + + assert success is True + + subtensor.get_neuron_for_pubkey_and_subnet.assert_called_once_with( + fake_wallet.hotkey.ss58_address, + netuid=1, + block_hash=mock_substrate.get_chain_head.return_value, + ) + + assert_submit_signed_extrinsic( + mock_substrate, + fake_wallet.coldkey, + call_module="SubtensorModule", + call_function="burned_register", + call_params={ + "netuid": 1, + "hotkey": fake_wallet.hotkey.ss58_address, + }, + wait_for_finalization=True, + wait_for_inclusion=False, + ) + + +@pytest.mark.asyncio +async def test_burned_register_on_root(mock_substrate, subtensor, fake_wallet, mocker): + mock_substrate.submit_extrinsic.return_value = mocker.AsyncMock( + is_success=mocker.AsyncMock(return_value=True)(), + ) + mocker.patch.object( + subtensor, + "get_balance", + return_value=Balance(1), + ) + mocker.patch.object( + subtensor, + "is_hotkey_registered", + return_value=False, + ) + + success = await subtensor.burned_register( + fake_wallet, + netuid=0, + ) + + assert success is True + + subtensor.is_hotkey_registered.assert_called_once_with( + netuid=0, + hotkey_ss58=fake_wallet.hotkey.ss58_address, + ) + + assert_submit_signed_extrinsic( + mock_substrate, + fake_wallet.coldkey, + call_module="SubtensorModule", + call_function="root_register", + call_params={ + "hotkey": fake_wallet.hotkey.ss58_address, + }, + wait_for_finalization=True, + wait_for_inclusion=False, + ) + + @pytest.mark.asyncio async def test_encode_params(subtensor, mocker): """Tests encode_params happy path.""" @@ -601,10 +681,10 @@ async def test_get_balance(subtensor, mocker): @pytest.mark.parametrize("balance", [100, 100.1]) @pytest.mark.asyncio -async def test_get_transfer_fee(subtensor, mocker, balance): +async def test_get_transfer_fee(subtensor, fake_wallet, mocker, balance): """Tests get_transfer_fee method.""" # Preps - fake_wallet = mocker.Mock(coldkeypub="coldkeypub", autospec=Wallet) + fake_wallet.coldkeypub = "coldkeypub" fake_dest = "fake_dest" fake_value = Balance(balance) @@ -832,9 +912,9 @@ async def test_filter_netuids_by_registered_hotkeys( ): """Tests filter_netuids_by_registered_hotkeys method.""" # Preps - fake_wallet_1 = mocker.Mock(autospec=Wallet) + fake_wallet_1 = mocker.Mock(spec_set=Wallet) fake_wallet_1.hotkey.ss58_address = "ss58_address_1" - fake_wallet_2 = mocker.Mock(autospec=Wallet) + fake_wallet_2 = mocker.Mock(spec_set=Wallet) fake_wallet_2.hotkey.ss58_address = "ss58_address_2" fake_all_netuids = all_netuids @@ -1557,7 +1637,6 @@ async def test_get_hotkey_owner_successful(subtensor, mocker): # Preps fake_hotkey_ss58 = "valid_hotkey" fake_block_hash = "block_hash" - fake_owner_account_id = "owner_account_id" mocked_query = mocker.AsyncMock(return_value="decoded_owner_account_id") subtensor.substrate.query = mocked_query @@ -1611,11 +1690,12 @@ async def test_get_hotkey_owner_non_existent_hotkey(subtensor, mocker): @pytest.mark.asyncio -async def test_sign_and_send_extrinsic_success_finalization(subtensor, mocker): +async def test_sign_and_send_extrinsic_success_finalization( + subtensor, fake_wallet, mocker +): """Tests sign_and_send_extrinsic when the extrinsic is successfully finalized.""" # Preps fake_call = mocker.Mock() - fake_wallet = mocker.Mock() fake_extrinsic = mocker.Mock() fake_response = mocker.Mock() @@ -1653,11 +1733,12 @@ async def fake_is_success(): @pytest.mark.asyncio -async def test_sign_and_send_extrinsic_error_finalization(subtensor, mocker): +async def test_sign_and_send_extrinsic_error_finalization( + subtensor, fake_wallet, mocker +): """Tests sign_and_send_extrinsic when the extrinsic is error finalized.""" # Preps fake_call = mocker.Mock() - fake_wallet = mocker.Mock() fake_extrinsic = mocker.Mock() fake_response = mocker.Mock() @@ -1704,12 +1785,11 @@ async def fake_error_message(): @pytest.mark.asyncio async def test_sign_and_send_extrinsic_success_without_inclusion_finalization( - subtensor, mocker + subtensor, fake_wallet, mocker ): """Tests sign_and_send_extrinsic when extrinsic is submitted without waiting for inclusion or finalization.""" # Preps fake_call = mocker.Mock() - fake_wallet = mocker.Mock() fake_extrinsic = mocker.Mock() mocked_create_signed_extrinsic = mocker.AsyncMock(return_value=fake_extrinsic) @@ -1741,11 +1821,12 @@ async def test_sign_and_send_extrinsic_success_without_inclusion_finalization( @pytest.mark.asyncio -async def test_sign_and_send_extrinsic_substrate_request_exception(subtensor, mocker): +async def test_sign_and_send_extrinsic_substrate_request_exception( + subtensor, fake_wallet, mocker +): """Tests sign_and_send_extrinsic when SubstrateRequestException is raised.""" # Preps fake_call = mocker.Mock() - fake_wallet = mocker.Mock() fake_extrinsic = mocker.Mock() fake_exception = async_subtensor.SubstrateRequestException("Test Exception") @@ -2343,10 +2424,9 @@ async def test_get_subnet_reveal_period_epochs(subtensor, mocker): @pytest.mark.asyncio -async def test_transfer_success(subtensor, mocker): +async def test_transfer_success(subtensor, fake_wallet, mocker): """Tests transfer when the transfer is successful.""" # Preps - fake_wallet = mocker.Mock() fake_destination = "destination_address" fake_amount = Balance.from_tao(100.0) fake_transfer_all = False @@ -2379,10 +2459,9 @@ async def test_transfer_success(subtensor, mocker): @pytest.mark.asyncio -async def test_register_success(subtensor, mocker): +async def test_register_success(subtensor, fake_wallet, mocker): """Tests register when there is enough balance and registration succeeds.""" # Preps - fake_wallet = mocker.Mock() fake_netuid = 1 mocked_register_extrinsic = mocker.AsyncMock() @@ -2413,10 +2492,80 @@ async def test_register_success(subtensor, mocker): @pytest.mark.asyncio -async def test_set_weights_success(subtensor, mocker): +async def test_set_delegate_take_equal(subtensor, fake_wallet, mocker): + mocker.patch.object(subtensor, "get_delegate_take", return_value=0.18) + + await subtensor.set_delegate_take( + fake_wallet, + fake_wallet.hotkey.ss58_address, + 0.18, + ) + + subtensor.substrate.submit_extrinsic.assert_not_called() + + +@pytest.mark.asyncio +async def test_set_delegate_take_increase( + mock_substrate, subtensor, fake_wallet, mocker +): + mock_substrate.submit_extrinsic.return_value = mocker.Mock( + is_success=mocker.AsyncMock(return_value=True)(), + ) + mocker.patch.object(subtensor, "get_delegate_take", return_value=0.18) + + await subtensor.set_delegate_take( + fake_wallet, + fake_wallet.hotkey.ss58_address, + 0.2, + ) + + assert_submit_signed_extrinsic( + mock_substrate, + fake_wallet.coldkey, + call_module="SubtensorModule", + call_function="increase_take", + call_params={ + "hotkey": fake_wallet.hotkey.ss58_address, + "take": 13107, + }, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + +@pytest.mark.asyncio +async def test_set_delegate_take_decrease( + mock_substrate, subtensor, fake_wallet, mocker +): + mock_substrate.submit_extrinsic.return_value = mocker.Mock( + is_success=mocker.AsyncMock(return_value=True)(), + ) + mocker.patch.object(subtensor, "get_delegate_take", return_value=0.18) + + await subtensor.set_delegate_take( + fake_wallet, + fake_wallet.hotkey.ss58_address, + 0.1, + ) + + assert_submit_signed_extrinsic( + mock_substrate, + fake_wallet.coldkey, + call_module="SubtensorModule", + call_function="decrease_take", + call_params={ + "hotkey": fake_wallet.hotkey.ss58_address, + "take": 6553, + }, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + +@pytest.mark.asyncio +async def test_set_weights_success(subtensor, fake_wallet, mocker): """Tests set_weights with successful weight setting on the first try.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_uids = [1, 2, 3] fake_weights = [0.3, 0.5, 0.2] @@ -2471,10 +2620,9 @@ async def test_set_weights_success(subtensor, mocker): @pytest.mark.asyncio -async def test_set_weights_with_exception(subtensor, mocker): +async def test_set_weights_with_exception(subtensor, fake_wallet, mocker): """Tests set_weights when set_weights_extrinsic raises an exception.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_uids = [1, 2, 3] fake_weights = [0.3, 0.5, 0.2] @@ -2516,10 +2664,9 @@ async def test_set_weights_with_exception(subtensor, mocker): @pytest.mark.asyncio -async def test_root_set_weights_success(subtensor, mocker): +async def test_root_set_weights_success(subtensor, fake_wallet, mocker): """Tests root_set_weights when the setting of weights is successful.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_netuids = [1, 2, 3] fake_weights = [0.3, 0.5, 0.2] @@ -2558,10 +2705,9 @@ async def test_root_set_weights_success(subtensor, mocker): @pytest.mark.asyncio -async def test_commit_weights_success(subtensor, mocker): +async def test_commit_weights_success(subtensor, fake_wallet, mocker): """Tests commit_weights when the weights are committed successfully.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_salt = [12345, 67890] fake_uids = [1, 2, 3] @@ -2610,10 +2756,9 @@ async def test_commit_weights_success(subtensor, mocker): @pytest.mark.asyncio -async def test_commit_weights_with_exception(subtensor, mocker): +async def test_commit_weights_with_exception(subtensor, fake_wallet, mocker): """Tests commit_weights when an exception is raised during weight commitment.""" # Preps - fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_salt = [12345, 67890] fake_uids = [1, 2, 3] @@ -2678,10 +2823,9 @@ async def test_get_all_subnets_info_success(mocker, subtensor): @pytest.mark.asyncio -async def test_set_subnet_identity(mocker, subtensor): +async def test_set_subnet_identity(mocker, subtensor, fake_wallet): """Verify that subtensor method `set_subnet_identity` calls proper function with proper arguments.""" # Preps - fake_wallet = mocker.Mock() fake_netuid = 123 fake_subnet_identity = mocker.MagicMock() diff --git a/tests/unit_tests/test_chain_data.py b/tests/unit_tests/test_chain_data.py index 63d8a69365..b2274f4a4f 100644 --- a/tests/unit_tests/test_chain_data.py +++ b/tests/unit_tests/test_chain_data.py @@ -2,8 +2,7 @@ import torch from async_substrate_interface.utils import json -from bittensor.core.chain_data import AxonInfo, DelegateInfo -from bittensor.core.chain_data.utils import ChainDataType +from bittensor.core.chain_data import AxonInfo RAOPERTAO = 10**18 diff --git a/tests/unit_tests/test_errors.py b/tests/unit_tests/test_errors.py new file mode 100644 index 0000000000..8a556b22e4 --- /dev/null +++ b/tests/unit_tests/test_errors.py @@ -0,0 +1,49 @@ +from bittensor.core.errors import ( + ChainError, + HotKeyAccountNotExists, +) + + +def test_from_error(): + error = { + "type": "Module", + "name": "HotKeyAccountNotExists", + "docs": ["The hotkey does not exists"], + } + + exception = ChainError.from_error(error) + + assert isinstance(exception, HotKeyAccountNotExists) + assert exception.args[0] == "The hotkey does not exists" + + +def test_from_error_unsupported_exception(): + error = { + "type": "Module", + "name": "UnknownException", + "docs": ["Unknown"], + } + + exception = ChainError.from_error(error) + + assert isinstance(exception, ChainError) + assert exception.args[0] == error + + +def test_from_error_new_exception(): + error = { + "type": "Module", + "name": "NewException", + "docs": ["New"], + } + + exception = ChainError.from_error(error) + + assert isinstance(exception, ChainError) + + class NewException(ChainError): + pass + + exception = ChainError.from_error(error) + + assert isinstance(exception, NewException) diff --git a/tests/unit_tests/test_subnets.py b/tests/unit_tests/test_subnets.py index 9cec02e935..00a5276264 100644 --- a/tests/unit_tests/test_subnets.py +++ b/tests/unit_tests/test_subnets.py @@ -1,5 +1,4 @@ import pytest -from mpmath.ctx_mp_python import return_mpc from bittensor.utils import subnets @@ -15,11 +14,10 @@ def process_responses(self, responses): return responses -def test_instance_creation(mocker): +def test_instance_creation(fake_wallet, mocker): """Test the creation of a MySubnetsAPI instance.""" # Prep mocked_dendrite = mocker.patch.object(subnets, "Dendrite") - fake_wallet = mocker.MagicMock() # Call instance = MySubnetsAPI(fake_wallet) @@ -32,7 +30,7 @@ def test_instance_creation(mocker): @pytest.mark.asyncio -async def test_query_api(mocker): +async def test_query_api(fake_wallet, mocker): """Test querying the MySubnetsAPI instance asynchronously.""" # Prep mocked_async_dendrite = mocker.AsyncMock() @@ -40,7 +38,6 @@ async def test_query_api(mocker): subnets, "Dendrite", return_value=mocked_async_dendrite ) - fake_wallet = mocker.MagicMock() fake_axon = mocker.MagicMock() mocked_synapse = mocker.MagicMock() @@ -60,7 +57,7 @@ async def test_query_api(mocker): @pytest.mark.asyncio -async def test_test_instance_call(mocker): +async def test_test_instance_call(fake_wallet, mocker): """Test the MySubnetsAPI instance call with asynchronous handling.""" # Prep mocked_async_dendrite = mocker.AsyncMock() @@ -70,7 +67,6 @@ async def test_test_instance_call(mocker): mocked_query_api = mocker.patch.object( MySubnetsAPI, "query_api", new=mocker.AsyncMock() ) - fake_wallet = mocker.MagicMock() fake_axon = mocker.MagicMock() # Call diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index c1844ff63d..2d7f1efb78 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -2,14 +2,14 @@ # Copyright © 2024 Opentensor Foundation # # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation # the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all copies or substantial portions of # the Software. # -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO # THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER @@ -75,7 +75,7 @@ def call_params_with_certificate(): return params -def test_methods_comparable(mock_substrate_interface): +def test_methods_comparable(mock_substrate): """Verifies that methods in sync and async Subtensors are comparable.""" # Preps subtensor = Subtensor(_mock=True) @@ -858,7 +858,7 @@ def test_get_subnet_hyperparameters_success(mocker, subtensor): ) # Call - result = subtensor.get_subnet_hyperparameters(netuid, block) + subtensor.get_subnet_hyperparameters(netuid, block) # Asserts subtensor.query_runtime_api.assert_called_once_with( @@ -1166,10 +1166,9 @@ def test_is_hotkey_registered_with_netuid(subtensor, mocker): assert result == mocked_is_hotkey_registered_on_subnet.return_value -def test_set_weights(subtensor, mocker): +def test_set_weights(subtensor, mocker, fake_wallet): """Successful set_weights call.""" # Preps - fake_wallet = mocker.MagicMock() fake_netuid = 1 fake_uids = [2, 4] fake_weights = [0.4, 0.6] @@ -1268,10 +1267,9 @@ def test_get_block_hash(subtensor, mocker): assert result == subtensor.substrate.get_block_hash.return_value -def test_commit(subtensor, mocker): +def test_commit(subtensor, fake_wallet, mocker): """Test successful commit call.""" # Preps - fake_wallet = mocker.MagicMock() fake_netuid = 1 fake_data = "some data to network" mocked_publish_metadata = mocker.patch.object(subtensor_module, "publish_metadata") @@ -1313,10 +1311,9 @@ def test_subnetwork_n(subtensor, mocker): assert result == mocked_get_hyperparameter.return_value -def test_transfer(subtensor, mocker): +def test_transfer(subtensor, fake_wallet, mocker): """Tests successful transfer call.""" # Prep - fake_wallet = mocker.MagicMock() fake_dest = "SS58PUBLICKEY" fake_amount = 1.1 fake_wait_for_inclusion = True @@ -1478,11 +1475,10 @@ def test_neuron_for_uid_success(subtensor, mocker): ], ) def test_do_serve_axon_is_success( - subtensor, mocker, fake_call_params, expected_call_function + subtensor, fake_wallet, mocker, fake_call_params, expected_call_function ): """Successful do_serve_axon call.""" # Prep - fake_wallet = mocker.MagicMock() fake_wait_for_inclusion = True fake_wait_for_finalization = True @@ -1520,10 +1516,9 @@ def test_do_serve_axon_is_success( assert result[1] is None -def test_do_serve_axon_is_not_success(subtensor, mocker, fake_call_params): +def test_do_serve_axon_is_not_success(subtensor, fake_wallet, mocker, fake_call_params): """Unsuccessful do_serve_axon call.""" # Prep - fake_wallet = mocker.MagicMock() fake_wait_for_inclusion = True fake_wait_for_finalization = True @@ -1562,10 +1557,9 @@ def test_do_serve_axon_is_not_success(subtensor, mocker, fake_call_params): ) -def test_do_serve_axon_no_waits(subtensor, mocker, fake_call_params): +def test_do_serve_axon_no_waits(subtensor, fake_wallet, mocker, fake_call_params): """Unsuccessful do_serve_axon call.""" # Prep - fake_wallet = mocker.MagicMock() fake_wait_for_inclusion = False fake_wait_for_finalization = False @@ -1884,10 +1878,9 @@ def test_max_weight_limit(subtensor, mocker): assert result == mocked_u16_normalized_float.return_value -def test_get_transfer_fee(subtensor, mocker): +def test_get_transfer_fee(subtensor, fake_wallet, mocker): """Successful get_transfer_fee call.""" # Preps - fake_wallet = mocker.MagicMock() fake_dest = "SS58ADDRESS" value = Balance(1) @@ -1937,10 +1930,9 @@ def test_get_existential_deposit(subtensor, mocker): assert result == Balance.from_rao(value) -def test_commit_weights(subtensor, mocker): +def test_commit_weights(subtensor, fake_wallet, mocker): """Successful commit_weights call.""" # Preps - fake_wallet = mocker.MagicMock() netuid = 1 salt = [1, 3] uids = [2, 4] @@ -1991,10 +1983,9 @@ def test_commit_weights(subtensor, mocker): assert result == expected_result -def test_reveal_weights(subtensor, mocker): +def test_reveal_weights(subtensor, fake_wallet, mocker): """Successful test_reveal_weights call.""" # Preps - fake_wallet = mocker.MagicMock() netuid = 1 uids = [1, 2, 3, 4] weights = [0.1, 0.2, 0.3, 0.4] @@ -2030,10 +2021,9 @@ def test_reveal_weights(subtensor, mocker): ) -def test_reveal_weights_false(subtensor, mocker): +def test_reveal_weights_false(subtensor, fake_wallet, mocker): """Failed test_reveal_weights call.""" # Preps - fake_wallet = mocker.MagicMock() netuid = 1 uids = [1, 2, 3, 4] weights = [0.1, 0.2, 0.3, 0.4] @@ -2272,33 +2262,9 @@ def test_get_delegate_take_success(subtensor, mocker): assert result == subtensor_module.u16_normalized_float.return_value -def test_get_delegate_take_none(subtensor, mocker): - """Verify `get_delegate_take` method returns None.""" - # Preps - fake_hotkey_ss58 = "FAKE_SS58" - fake_block = 123 - - subtensor.query_subtensor = mocker.Mock(return_value=None) - mocker.patch.object(subtensor_module, "u16_normalized_float") - - # Call - result = subtensor.get_delegate_take(hotkey_ss58=fake_hotkey_ss58, block=fake_block) - - # Asserts - subtensor.query_subtensor.assert_called_once_with( - name="Delegates", - block=fake_block, - params=[fake_hotkey_ss58], - ) - - subtensor_module.u16_normalized_float.assert_not_called() - assert result is None - - -def test_networks_during_connection(mocker): +def test_networks_during_connection(mock_substrate, mocker): """Test networks during_connection.""" # Preps - mocker.patch.object(subtensor_module, "SubstrateInterface") mocker.patch("websockets.sync.client.connect") # Call for network in list(settings.NETWORK_MAP.keys()) + ["undefined"]: @@ -2848,10 +2814,9 @@ def test_is_hotkey_delegate_empty_list(mocker, subtensor): assert result is False -def test_add_stake_success(mocker, subtensor): +def test_add_stake_success(mocker, fake_wallet, subtensor): """Test add_stake returns True on successful staking.""" # Prep - fake_wallet = mocker.Mock() fake_hotkey_ss58 = "fake_hotkey" fake_amount = 10.0 @@ -2866,6 +2831,9 @@ def test_add_stake_success(mocker, subtensor): amount=fake_amount, wait_for_inclusion=True, wait_for_finalization=False, + safe_staking=False, + allow_partial_stake=False, + rate_tolerance=0.005, ) # Assertions @@ -2877,14 +2845,55 @@ def test_add_stake_success(mocker, subtensor): amount=Balance.from_rao(fake_amount), wait_for_inclusion=True, wait_for_finalization=False, + safe_staking=False, + allow_partial_stake=False, + rate_tolerance=0.005, ) assert result == mock_add_stake_extrinsic.return_value -def test_add_stake_multiple_success(mocker, subtensor): +def test_add_stake_with_safe_staking(mocker, fake_wallet, subtensor): + """Test add_stake with safe staking parameters enabled.""" + # Prep + fake_hotkey_ss58 = "fake_hotkey" + fake_amount = 10.0 + fake_rate_tolerance = 0.01 # 1% threshold + + mock_add_stake_extrinsic = mocker.patch.object( + subtensor_module, "add_stake_extrinsic" + ) + + # Call + result = subtensor.add_stake( + wallet=fake_wallet, + hotkey_ss58=fake_hotkey_ss58, + amount=fake_amount, + wait_for_inclusion=True, + wait_for_finalization=False, + safe_staking=True, + allow_partial_stake=False, + rate_tolerance=fake_rate_tolerance, + ) + + # Assertions + mock_add_stake_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + hotkey_ss58=fake_hotkey_ss58, + netuid=None, + amount=Balance.from_rao(fake_amount), + wait_for_inclusion=True, + wait_for_finalization=False, + safe_staking=True, + allow_partial_stake=False, + rate_tolerance=fake_rate_tolerance, + ) + assert result == mock_add_stake_extrinsic.return_value + + +def test_add_stake_multiple_success(mocker, fake_wallet, subtensor): """Test add_stake_multiple successfully stakes for all hotkeys.""" # Prep - fake_wallet = mocker.Mock() fake_hotkey_ss58 = ["fake_hotkey"] fake_amount = [10.0] @@ -2915,10 +2924,9 @@ def test_add_stake_multiple_success(mocker, subtensor): assert result == mock_add_stake_multiple_extrinsic.return_value -def test_unstake_success(mocker, subtensor): +def test_unstake_success(mocker, subtensor, fake_wallet): """Test unstake operation is successful.""" # Preps - fake_wallet = mocker.Mock() fake_hotkey_ss58 = "hotkey_1" fake_amount = 10.0 @@ -2931,6 +2939,9 @@ def test_unstake_success(mocker, subtensor): amount=fake_amount, wait_for_inclusion=True, wait_for_finalization=False, + safe_staking=False, + allow_partial_stake=False, + rate_tolerance=0.005, ) # Assertions @@ -2942,14 +2953,139 @@ def test_unstake_success(mocker, subtensor): amount=Balance.from_rao(fake_amount), wait_for_inclusion=True, wait_for_finalization=False, + safe_staking=False, + allow_partial_stake=False, + rate_tolerance=0.005, ) assert result == mock_unstake_extrinsic.return_value -def test_unstake_multiple_success(mocker, subtensor): +def test_unstake_with_safe_staking(mocker, subtensor, fake_wallet): + """Test unstake with safe staking parameters enabled.""" + fake_hotkey_ss58 = "hotkey_1" + fake_amount = 10.0 + fake_rate_tolerance = 0.01 # 1% threshold + + mock_unstake_extrinsic = mocker.patch.object(subtensor_module, "unstake_extrinsic") + + # Call + result = subtensor.unstake( + wallet=fake_wallet, + hotkey_ss58=fake_hotkey_ss58, + amount=fake_amount, + wait_for_inclusion=True, + wait_for_finalization=False, + safe_staking=True, + allow_partial_stake=True, + rate_tolerance=fake_rate_tolerance, + ) + + # Assertions + mock_unstake_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + hotkey_ss58=fake_hotkey_ss58, + netuid=None, + amount=Balance.from_rao(fake_amount), + wait_for_inclusion=True, + wait_for_finalization=False, + safe_staking=True, + allow_partial_stake=True, + rate_tolerance=fake_rate_tolerance, + ) + assert result == mock_unstake_extrinsic.return_value + + +def test_swap_stake_success(mocker, subtensor, fake_wallet): + """Test swap_stake operation is successful.""" + # Preps + fake_hotkey_ss58 = "hotkey_1" + fake_origin_netuid = 1 + fake_destination_netuid = 2 + fake_amount = 10.0 + + mock_swap_stake_extrinsic = mocker.patch.object( + subtensor_module, "swap_stake_extrinsic" + ) + + # Call + result = subtensor.swap_stake( + wallet=fake_wallet, + hotkey_ss58=fake_hotkey_ss58, + origin_netuid=fake_origin_netuid, + destination_netuid=fake_destination_netuid, + amount=fake_amount, + wait_for_inclusion=True, + wait_for_finalization=False, + safe_staking=False, + allow_partial_stake=False, + rate_tolerance=0.005, + ) + + # Assertions + mock_swap_stake_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + hotkey_ss58=fake_hotkey_ss58, + origin_netuid=fake_origin_netuid, + destination_netuid=fake_destination_netuid, + amount=Balance.from_rao(fake_amount), + wait_for_inclusion=True, + wait_for_finalization=False, + safe_staking=False, + allow_partial_stake=False, + rate_tolerance=0.005, + ) + assert result == mock_swap_stake_extrinsic.return_value + + +def test_swap_stake_with_safe_staking(mocker, subtensor, fake_wallet): + """Test swap_stake with safe staking parameters enabled.""" + # Preps + fake_hotkey_ss58 = "hotkey_1" + fake_origin_netuid = 1 + fake_destination_netuid = 2 + fake_amount = 10.0 + fake_rate_tolerance = 0.01 # 1% threshold + + mock_swap_stake_extrinsic = mocker.patch.object( + subtensor_module, "swap_stake_extrinsic" + ) + + # Call + result = subtensor.swap_stake( + wallet=fake_wallet, + hotkey_ss58=fake_hotkey_ss58, + origin_netuid=fake_origin_netuid, + destination_netuid=fake_destination_netuid, + amount=fake_amount, + wait_for_inclusion=True, + wait_for_finalization=False, + safe_staking=True, + allow_partial_stake=True, + rate_tolerance=fake_rate_tolerance, + ) + + # Assertions + mock_swap_stake_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + hotkey_ss58=fake_hotkey_ss58, + origin_netuid=fake_origin_netuid, + destination_netuid=fake_destination_netuid, + amount=Balance.from_rao(fake_amount), + wait_for_inclusion=True, + wait_for_finalization=False, + safe_staking=True, + allow_partial_stake=True, + rate_tolerance=fake_rate_tolerance, + ) + assert result == mock_swap_stake_extrinsic.return_value + + +def test_unstake_multiple_success(mocker, subtensor, fake_wallet): """Test unstake_multiple succeeds for all hotkeys.""" # Preps - fake_wallet = mocker.Mock() fake_hotkeys = ["hotkey_1", "hotkey_2"] fake_amounts = [10.0, 20.0] @@ -2980,10 +3116,9 @@ def test_unstake_multiple_success(mocker, subtensor): assert result == mock_unstake_multiple_extrinsic.return_value -def test_set_weights_with_commit_reveal_enabled(subtensor, mocker): +def test_set_weights_with_commit_reveal_enabled(subtensor, fake_wallet, mocker): """Test set_weights with commit_reveal_enabled is True.""" # Preps - fake_wallet = mocker.Mock() fake_netuid = 1 fake_uids = [1, 5] fake_weights = [0.1, 0.9] @@ -3052,10 +3187,9 @@ def test_connection_limit(mocker): Subtensor("test") -def test_set_subnet_identity(mocker, subtensor): +def test_set_subnet_identity(mocker, subtensor, fake_wallet): """Verify that subtensor method `set_subnet_identity` calls proper function with proper arguments.""" # Preps - fake_wallet = mocker.Mock() fake_netuid = 123 fake_subnet_identity = mocker.MagicMock() diff --git a/tests/unit_tests/test_subtensor_extended.py b/tests/unit_tests/test_subtensor_extended.py index ce33db130c..b0cd2afbf3 100644 --- a/tests/unit_tests/test_subtensor_extended.py +++ b/tests/unit_tests/test_subtensor_extended.py @@ -1,9 +1,9 @@ import unittest.mock -from typing import Optional import pytest -import bittensor.core.subtensor +import async_substrate_interface.errors + from bittensor.core.chain_data.axon_info import AxonInfo from bittensor.core.chain_data.chain_identity import ChainIdentity from bittensor.core.chain_data.delegate_info import DelegatedInfo, DelegateInfo @@ -13,64 +13,7 @@ from bittensor.core.chain_data.prometheus_info import PrometheusInfo from bittensor.core.chain_data.stake_info import StakeInfo from bittensor.utils.balance import Balance - - -def assert_submit_signed_extrinsic( - substrate, - keypair, - call_module, - call_function, - call_params: Optional[dict] = None, - era: Optional[dict] = None, - nonce: Optional[int] = None, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, -): - substrate.compose_call.assert_called_with( - call_module, - call_function, - call_params, - ) - - extrinsic = { - "call": substrate.compose_call.return_value, - "keypair": keypair, - } - - if era: - extrinsic["era"] = era - - if nonce: - extrinsic["nonce"] = nonce - - substrate.create_signed_extrinsic.assert_called_with( - **extrinsic, - ) - - substrate.submit_extrinsic.assert_called_with( - substrate.create_signed_extrinsic.return_value, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - -@pytest.fixture -def mock_substrate(): - with unittest.mock.patch( - "bittensor.core.subtensor.SubstrateInterface", - autospec=True, - ) as mocked: - yield mocked.return_value - - -@pytest.fixture -def subtensor(mock_substrate): - return bittensor.core.subtensor.Subtensor() - - -@pytest.fixture -def wallet(): - return unittest.mock.Mock() +from tests.helpers.helpers import assert_submit_signed_extrinsic @pytest.fixture @@ -223,7 +166,7 @@ def test_bonds(mock_substrate, subtensor, mocker): ) -def test_burned_register(mock_substrate, subtensor, wallet, mocker): +def test_burned_register(mock_substrate, subtensor, fake_wallet, mocker): mocker.patch.object( subtensor, "get_neuron_for_pubkey_and_subnet", @@ -232,26 +175,63 @@ def test_burned_register(mock_substrate, subtensor, wallet, mocker): mocker.patch.object(subtensor, "get_balance") success = subtensor.burned_register( - wallet, + fake_wallet, netuid=1, ) assert success is True subtensor.get_neuron_for_pubkey_and_subnet.assert_called_once_with( - wallet.hotkey.ss58_address, + fake_wallet.hotkey.ss58_address, netuid=1, block=mock_substrate.get_block_number.return_value, ) assert_submit_signed_extrinsic( mock_substrate, - wallet.coldkey, + fake_wallet.coldkey, call_module="SubtensorModule", call_function="burned_register", call_params={ "netuid": 1, - "hotkey": wallet.hotkey.ss58_address, + "hotkey": fake_wallet.hotkey.ss58_address, + }, + wait_for_finalization=True, + wait_for_inclusion=False, + ) + + +def test_burned_register_on_root(mock_substrate, subtensor, fake_wallet, mocker): + mocker.patch.object( + subtensor, + "get_balance", + return_value=Balance(1), + ) + mocker.patch.object( + subtensor, + "is_hotkey_registered", + return_value=False, + ) + + success = subtensor.burned_register( + fake_wallet, + netuid=0, + ) + + assert success is True + + subtensor.is_hotkey_registered.assert_called_once_with( + netuid=0, + hotkey_ss58=fake_wallet.hotkey.ss58_address, + ) + + assert_submit_signed_extrinsic( + mock_substrate, + fake_wallet.coldkey, + call_module="SubtensorModule", + call_function="root_register", + call_params={ + "hotkey": fake_wallet.hotkey.ss58_address, }, wait_for_finalization=True, wait_for_inclusion=False, @@ -381,7 +361,7 @@ def test_get_block_hash_none(mock_substrate, subtensor): mock_substrate.get_chain_head.assert_called_once() -def test_get_children(mock_substrate, subtensor, wallet): +def test_get_children(mock_substrate, subtensor, fake_wallet): mock_substrate.query.return_value.value = [ ( 2**64 - 1, @@ -411,7 +391,7 @@ def test_get_children(mock_substrate, subtensor, wallet): ) -def test_get_current_weight_commit_info(mock_substrate, subtensor, wallet, mocker): +def test_get_current_weight_commit_info(mock_substrate, subtensor, fake_wallet, mocker): mock_substrate.query_map.return_value.records = [ ( mocker.ANY, @@ -614,7 +594,7 @@ def test_get_stake_for_coldkey(mock_substrate, subtensor, mocker): def test_filter_netuids_by_registered_hotkeys( - mock_substrate, subtensor, wallet, mocker + mock_substrate, subtensor, fake_wallet, mocker ): mock_substrate.query_map.return_value = mocker.MagicMock( **{ @@ -640,7 +620,7 @@ def test_filter_netuids_by_registered_hotkeys( result = subtensor.filter_netuids_by_registered_hotkeys( all_netuids=[0, 1, 2], filter_for_netuids=[2], - all_hotkeys=[wallet], + all_hotkeys=[fake_wallet], block=10, ) @@ -650,7 +630,7 @@ def test_filter_netuids_by_registered_hotkeys( mock_substrate.query_map.assert_called_once_with( module="SubtensorModule", storage_function="IsNetworkMember", - params=[wallet.hotkey.ss58_address], + params=[fake_wallet.hotkey.ss58_address], block_hash=mock_substrate.get_block_hash.return_value, ) @@ -675,9 +655,9 @@ def test_last_drand_round(mock_substrate, subtensor): False, ), ) -def test_move_stake(mock_substrate, subtensor, wallet, wait): +def test_move_stake(mock_substrate, subtensor, fake_wallet, wait): success = subtensor.move_stake( - wallet, + fake_wallet, origin_hotkey="origin_hotkey", origin_netuid=1, destination_hotkey="destination_hotkey", @@ -691,7 +671,7 @@ def test_move_stake(mock_substrate, subtensor, wallet, wait): assert_submit_signed_extrinsic( mock_substrate, - wallet.coldkey, + fake_wallet.coldkey, call_module="SubtensorModule", call_function="move_stake", call_params={ @@ -706,11 +686,11 @@ def test_move_stake(mock_substrate, subtensor, wallet, wait): ) -def test_move_stake_insufficient_stake(mock_substrate, subtensor, wallet, mocker): +def test_move_stake_insufficient_stake(mock_substrate, subtensor, fake_wallet, mocker): mocker.patch.object(subtensor, "get_stake", return_value=Balance(0)) success = subtensor.move_stake( - wallet, + fake_wallet, origin_hotkey="origin_hotkey", origin_netuid=1, destination_hotkey="destination_hotkey", @@ -723,14 +703,14 @@ def test_move_stake_insufficient_stake(mock_substrate, subtensor, wallet, mocker mock_substrate.submit_extrinsic.assert_not_called() -def test_move_stake_error(mock_substrate, subtensor, wallet, mocker): +def test_move_stake_error(mock_substrate, subtensor, fake_wallet, mocker): mock_substrate.submit_extrinsic.return_value = mocker.Mock( error_message="ERROR", is_success=False, ) success = subtensor.move_stake( - wallet, + fake_wallet, origin_hotkey="origin_hotkey", origin_netuid=1, destination_hotkey="destination_hotkey", @@ -742,7 +722,7 @@ def test_move_stake_error(mock_substrate, subtensor, wallet, mocker): assert_submit_signed_extrinsic( mock_substrate, - wallet.coldkey, + fake_wallet.coldkey, call_module="SubtensorModule", call_function="move_stake", call_params={ @@ -757,11 +737,11 @@ def test_move_stake_error(mock_substrate, subtensor, wallet, mocker): ) -def test_move_stake_exception(mock_substrate, subtensor, wallet): +def test_move_stake_exception(mock_substrate, subtensor, fake_wallet): mock_substrate.submit_extrinsic.side_effect = RuntimeError success = subtensor.move_stake( - wallet, + fake_wallet, origin_hotkey="origin_hotkey", origin_netuid=1, destination_hotkey="destination_hotkey", @@ -773,7 +753,7 @@ def test_move_stake_exception(mock_substrate, subtensor, wallet): assert_submit_signed_extrinsic( mock_substrate, - wallet.coldkey, + fake_wallet.coldkey, call_module="SubtensorModule", call_function="move_stake", call_params={ @@ -898,10 +878,67 @@ def test_neurons_lite(mock_substrate, subtensor, mock_neuron_info): ) +def test_set_delegate_take_equal(mock_substrate, subtensor, fake_wallet, mocker): + mocker.patch.object(subtensor, "get_delegate_take", return_value=0.18) + + subtensor.set_delegate_take( + fake_wallet, + fake_wallet.hotkey.ss58_address, + 0.18, + ) + + mock_substrate.submit_extrinsic.assert_not_called() + + +def test_set_delegate_take_increase(mock_substrate, subtensor, fake_wallet, mocker): + mocker.patch.object(subtensor, "get_delegate_take", return_value=0.18) + + subtensor.set_delegate_take( + fake_wallet, + fake_wallet.hotkey.ss58_address, + 0.2, + ) + + assert_submit_signed_extrinsic( + mock_substrate, + fake_wallet.coldkey, + call_module="SubtensorModule", + call_function="increase_take", + call_params={ + "hotkey": fake_wallet.hotkey.ss58_address, + "take": 13107, + }, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + +def test_set_delegate_take_decrease(mock_substrate, subtensor, fake_wallet, mocker): + mocker.patch.object(subtensor, "get_delegate_take", return_value=0.18) + + subtensor.set_delegate_take( + fake_wallet, + fake_wallet.hotkey.ss58_address, + 0.1, + ) + + assert_submit_signed_extrinsic( + mock_substrate, + fake_wallet.coldkey, + call_module="SubtensorModule", + call_function="decrease_take", + call_params={ + "hotkey": fake_wallet.hotkey.ss58_address, + "take": 6553, + }, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + def test_subnet(mock_substrate, subtensor, mock_dynamic_info): mock_substrate.runtime_call.return_value.decode.return_value = mock_dynamic_info - subtensor = bittensor.core.subtensor.Subtensor() result = subtensor.subnet(netuid=0) assert result == DynamicInfo( @@ -946,18 +983,18 @@ def test_subtensor_contextmanager(mock_substrate, subtensor): mock_substrate.close.assert_called_once() -def test_swap_stake(mock_substrate, subtensor, wallet, mocker): +def test_swap_stake(mock_substrate, subtensor, fake_wallet, mocker): mocker.patch.object(subtensor, "get_stake", return_value=Balance(1000)) mocker.patch.object( subtensor, "get_hotkey_owner", autospec=True, - return_value=wallet.coldkeypub.ss58_address, + return_value=fake_wallet.coldkeypub.ss58_address, ) result = subtensor.swap_stake( - wallet, - wallet.hotkey.ss58_address, + fake_wallet, + fake_wallet.hotkey.ss58_address, origin_netuid=1, destination_netuid=2, amount=Balance(999), @@ -967,11 +1004,11 @@ def test_swap_stake(mock_substrate, subtensor, wallet, mocker): assert_submit_signed_extrinsic( mock_substrate, - wallet.coldkey, + fake_wallet.coldkey, call_module="SubtensorModule", call_function="swap_stake", call_params={ - "hotkey": wallet.hotkey.ss58_address, + "hotkey": fake_wallet.hotkey.ss58_address, "origin_netuid": 1, "destination_netuid": 2, "alpha_amount": 999, @@ -1027,7 +1064,7 @@ def test_query_identity(mock_substrate, subtensor, query, result): ) -def test_register(mock_substrate, subtensor, wallet, mocker): +def test_register(mock_substrate, subtensor, fake_wallet, mocker): create_pow = mocker.patch( "bittensor.core.extrinsics.registration.create_pow", return_value=mocker.Mock( @@ -1044,20 +1081,20 @@ def test_register(mock_substrate, subtensor, wallet, mocker): ) result = subtensor.register( - wallet, + fake_wallet, netuid=1, ) assert result is True subtensor.get_neuron_for_pubkey_and_subnet.assert_called_once_with( - hotkey_ss58=wallet.hotkey.ss58_address, + hotkey_ss58=fake_wallet.hotkey.ss58_address, netuid=1, block=mock_substrate.get_block_number.return_value, ) create_pow.assert_called_once_with( subtensor=subtensor, - wallet=wallet, + wallet=fake_wallet, netuid=1, output_in_place=True, cuda=False, @@ -1068,13 +1105,13 @@ def test_register(mock_substrate, subtensor, wallet, mocker): assert_submit_signed_extrinsic( mock_substrate, - wallet.coldkey, + fake_wallet.coldkey, call_module="SubtensorModule", call_function="register", call_params={ "block_number": create_pow.return_value.block_number, - "coldkey": wallet.coldkeypub.ss58_address, - "hotkey": wallet.hotkey.ss58_address, + "coldkey": fake_wallet.coldkeypub.ss58_address, + "hotkey": fake_wallet.hotkey.ss58_address, "netuid": 1, "nonce": create_pow.return_value.nonce, "work": [1, 2, 3], @@ -1089,7 +1126,7 @@ def test_register(mock_substrate, subtensor, wallet, mocker): False, ], ) -def test_register_subnet(mock_substrate, subtensor, wallet, mocker, success): +def test_register_subnet(mock_substrate, subtensor, fake_wallet, mocker, success): mocker.patch.object(subtensor, "get_balance", return_value=Balance(100)) mocker.patch.object(subtensor, "get_subnet_burn_cost", return_value=Balance(10)) @@ -1098,29 +1135,31 @@ def test_register_subnet(mock_substrate, subtensor, wallet, mocker, success): ) result = subtensor.register_subnet( - wallet, + fake_wallet, ) assert result is success assert_submit_signed_extrinsic( mock_substrate, - wallet.coldkey, + fake_wallet.coldkey, call_module="SubtensorModule", call_function="register_network", call_params={ - "hotkey": wallet.hotkey.ss58_address, + "hotkey": fake_wallet.hotkey.ss58_address, "mechid": 1, }, ) -def test_register_subnet_insufficient_funds(mock_substrate, subtensor, wallet, mocker): +def test_register_subnet_insufficient_funds( + mock_substrate, subtensor, fake_wallet, mocker +): mocker.patch.object(subtensor, "get_balance", return_value=Balance(0)) mocker.patch.object(subtensor, "get_subnet_burn_cost", return_value=Balance(10)) success = subtensor.register_subnet( - wallet, + fake_wallet, ) assert success is False @@ -1128,7 +1167,7 @@ def test_register_subnet_insufficient_funds(mock_substrate, subtensor, wallet, m mock_substrate.submit_extrinsic.assert_not_called() -def test_root_register(mock_substrate, subtensor, wallet, mocker): +def test_root_register(mock_substrate, subtensor, fake_wallet, mocker): mocker.patch.object( subtensor, "get_balance", autospec=True, return_value=Balance(100) ) @@ -1137,33 +1176,35 @@ def test_root_register(mock_substrate, subtensor, wallet, mocker): subtensor, "is_hotkey_registered_on_subnet", autospec=True, return_value=False ) - success = subtensor.root_register(wallet) + success = subtensor.root_register(fake_wallet) assert success is True subtensor.get_balance.assert_called_once_with( - wallet.coldkeypub.ss58_address, + fake_wallet.coldkeypub.ss58_address, block=mock_substrate.get_block_number.return_value, ) subtensor.get_hyperparameter.assert_called_once() subtensor.is_hotkey_registered_on_subnet.assert_called_once_with( - wallet.hotkey.ss58_address, + fake_wallet.hotkey.ss58_address, 0, None, ) assert_submit_signed_extrinsic( mock_substrate, - wallet.coldkey, + fake_wallet.coldkey, call_module="SubtensorModule", call_function="root_register", call_params={ - "hotkey": wallet.hotkey.ss58_address, + "hotkey": fake_wallet.hotkey.ss58_address, }, ) -def test_root_register_is_already_registered(mock_substrate, subtensor, wallet, mocker): +def test_root_register_is_already_registered( + mock_substrate, subtensor, fake_wallet, mocker +): mocker.patch.object( subtensor, "get_balance", autospec=True, return_value=Balance(100) ) @@ -1172,36 +1213,19 @@ def test_root_register_is_already_registered(mock_substrate, subtensor, wallet, subtensor, "is_hotkey_registered_on_subnet", autospec=True, return_value=True ) - success = subtensor.root_register(wallet) + success = subtensor.root_register(fake_wallet) assert success is True subtensor.is_hotkey_registered_on_subnet.assert_called_once_with( - wallet.hotkey.ss58_address, + fake_wallet.hotkey.ss58_address, 0, None, ) mock_substrate.submit_extrinsic.assert_not_called() -def test_root_register_insufficient_balance(mock_substrate, subtensor, wallet, mocker): - mocker.patch.object( - subtensor, "get_balance", autospec=True, return_value=Balance(1) - ) - mocker.patch.object(subtensor, "get_hyperparameter", autospec=True, return_value=10) - - success = subtensor.root_register(wallet) - - assert success is False - - subtensor.get_balance.assert_called_once_with( - wallet.coldkeypub.ss58_address, - block=mock_substrate.get_block_number.return_value, - ) - mock_substrate.submit_extrinsic.assert_not_called() - - -def test_root_set_weights(mock_substrate, subtensor, wallet, mocker): +def test_root_set_weights(mock_substrate, subtensor, fake_wallet, mocker): MIN_ALLOWED_WEIGHTS = 0 MAX_WEIGHTS_LIMIT = 1 @@ -1217,7 +1241,7 @@ def test_root_set_weights(mock_substrate, subtensor, wallet, mocker): ) subtensor.root_set_weights( - wallet, + fake_wallet, netuids=[1, 2], weights=[0.5, 0.5], ) @@ -1231,12 +1255,12 @@ def test_root_set_weights(mock_substrate, subtensor, wallet, mocker): mock_substrate.query.assert_called_once_with( "SubtensorModule", "Uids", - [0, wallet.hotkey.ss58_address], + [0, fake_wallet.hotkey.ss58_address], ) assert_submit_signed_extrinsic( mock_substrate, - wallet.coldkey, + fake_wallet.coldkey, call_module="SubtensorModule", call_function="set_root_weights", call_params={ @@ -1244,7 +1268,7 @@ def test_root_set_weights(mock_substrate, subtensor, wallet, mocker): "weights": [65535, 65535], "netuid": 0, "version_key": 0, - "hotkey": wallet.hotkey.ss58_address, + "hotkey": fake_wallet.hotkey.ss58_address, }, era={ "period": 5, @@ -1254,11 +1278,11 @@ def test_root_set_weights(mock_substrate, subtensor, wallet, mocker): ) -def test_root_set_weights_no_uid(mock_substrate, subtensor, wallet, mocker): +def test_root_set_weights_no_uid(mock_substrate, subtensor, fake_wallet, mocker): mock_substrate.query.return_value = None success = subtensor.root_set_weights( - wallet, + fake_wallet, netuids=[1, 2], weights=[0.5, 0.5], ) @@ -1268,13 +1292,13 @@ def test_root_set_weights_no_uid(mock_substrate, subtensor, wallet, mocker): mock_substrate.query.assert_called_once_with( "SubtensorModule", "Uids", - [0, wallet.hotkey.ss58_address], + [0, fake_wallet.hotkey.ss58_address], ) mock_substrate.submit_extrinsic.assert_not_called() def test_root_set_weights_min_allowed_weights( - mock_substrate, subtensor, wallet, mocker + mock_substrate, subtensor, fake_wallet, mocker ): mocker.patch.object( subtensor, @@ -1289,7 +1313,7 @@ def test_root_set_weights_min_allowed_weights( match="The minimum number of weights required to set weights is 5, got 2", ): subtensor.root_set_weights( - wallet, + fake_wallet, netuids=[1, 2], weights=[0.5, 0.5], ) @@ -1298,25 +1322,25 @@ def test_root_set_weights_min_allowed_weights( mock_substrate.submit_extrinsic.assert_not_called() -def test_sign_and_send_extrinsic(mock_substrate, subtensor, wallet, mocker): +def test_sign_and_send_extrinsic(mock_substrate, subtensor, fake_wallet, mocker): call = mocker.Mock() subtensor.sign_and_send_extrinsic( call, - wallet, + fake_wallet, use_nonce=True, period=10, ) mock_substrate.get_account_next_index.assert_called_once_with( - wallet.hotkey.ss58_address, + fake_wallet.hotkey.ss58_address, ) mock_substrate.create_signed_extrinsic.assert_called_once_with( call=call, era={ "period": 10, }, - keypair=wallet.coldkey, + keypair=fake_wallet.coldkey, nonce=mock_substrate.get_account_next_index.return_value, ) mock_substrate.submit_extrinsic.assert_called_once_with( @@ -1326,6 +1350,27 @@ def test_sign_and_send_extrinsic(mock_substrate, subtensor, wallet, mocker): ) +def test_sign_and_send_extrinsic_raises_error( + mock_substrate, subtensor, fake_wallet, mocker +): + mock_substrate.submit_extrinsic.return_value = mocker.Mock( + error_message={ + "name": "Exception", + }, + is_success=False, + ) + + with pytest.raises( + async_substrate_interface.errors.SubstrateRequestException, + match="{'name': 'Exception'}", + ): + subtensor.sign_and_send_extrinsic( + call=mocker.Mock(), + wallet=fake_wallet, + raise_error=True, + ) + + @pytest.mark.parametrize( "wait", ( @@ -1333,16 +1378,16 @@ def test_sign_and_send_extrinsic(mock_substrate, subtensor, wallet, mocker): False, ), ) -def test_transfer_stake(mock_substrate, subtensor, wallet, mocker, wait): +def test_transfer_stake(mock_substrate, subtensor, fake_wallet, mocker, wait): mocker.patch.object( subtensor, "get_hotkey_owner", autospec=True, - return_value=wallet.coldkeypub.ss58_address, + return_value=fake_wallet.coldkeypub.ss58_address, ) success = subtensor.transfer_stake( - wallet, + fake_wallet, "dest", "hotkey_ss58", origin_netuid=1, @@ -1356,7 +1401,7 @@ def test_transfer_stake(mock_substrate, subtensor, wallet, mocker, wait): assert_submit_signed_extrinsic( mock_substrate, - wallet.coldkey, + fake_wallet.coldkey, call_module="SubtensorModule", call_function="transfer_stake", call_params={ @@ -1383,17 +1428,19 @@ def test_transfer_stake(mock_substrate, subtensor, wallet, mocker, wait): RuntimeError, ), ) -def test_transfer_stake_error(mock_substrate, subtensor, wallet, mocker, side_effect): +def test_transfer_stake_error( + mock_substrate, subtensor, fake_wallet, mocker, side_effect +): mocker.patch.object( subtensor, "get_hotkey_owner", autospec=True, - return_value=wallet.coldkeypub.ss58_address, + return_value=fake_wallet.coldkeypub.ss58_address, ) mock_substrate.submit_extrinsic.return_value = side_effect success = subtensor.transfer_stake( - wallet, + fake_wallet, "dest", "hotkey_ss58", origin_netuid=1, @@ -1405,7 +1452,7 @@ def test_transfer_stake_error(mock_substrate, subtensor, wallet, mocker, side_ef assert_submit_signed_extrinsic( mock_substrate, - wallet.coldkey, + fake_wallet.coldkey, call_module="SubtensorModule", call_function="transfer_stake", call_params={ @@ -1420,7 +1467,7 @@ def test_transfer_stake_error(mock_substrate, subtensor, wallet, mocker, side_ef ) -def test_transfer_stake_non_owner(mock_substrate, subtensor, wallet, mocker): +def test_transfer_stake_non_owner(mock_substrate, subtensor, fake_wallet, mocker): mocker.patch.object( subtensor, "get_hotkey_owner", @@ -1429,7 +1476,7 @@ def test_transfer_stake_non_owner(mock_substrate, subtensor, wallet, mocker): ) success = subtensor.transfer_stake( - wallet, + fake_wallet, "dest", "hotkey_ss58", origin_netuid=1, @@ -1445,12 +1492,14 @@ def test_transfer_stake_non_owner(mock_substrate, subtensor, wallet, mocker): mock_substrate.submit_extrinsic.assert_not_called() -def test_transfer_stake_insufficient_stake(mock_substrate, subtensor, wallet, mocker): +def test_transfer_stake_insufficient_stake( + mock_substrate, subtensor, fake_wallet, mocker +): mocker.patch.object( subtensor, "get_hotkey_owner", autospec=True, - return_value=wallet.coldkeypub.ss58_address, + return_value=fake_wallet.coldkeypub.ss58_address, ) with unittest.mock.patch.object( @@ -1459,7 +1508,7 @@ def test_transfer_stake_insufficient_stake(mock_substrate, subtensor, wallet, mo return_value=Balance(0), ): success = subtensor.transfer_stake( - wallet, + fake_wallet, "dest", "hotkey_ss58", origin_netuid=1, diff --git a/tests/unit_tests/utils/test_utils.py b/tests/unit_tests/utils/test_utils.py index 4c2c6b8b08..9a6527d643 100644 --- a/tests/unit_tests/utils/test_utils.py +++ b/tests/unit_tests/utils/test_utils.py @@ -1,5 +1,4 @@ import pytest -from bittensor_wallet import Wallet from bittensor import warnings, __getattr__, version_split, logging, trace, debug, utils from bittensor.core.settings import SS58_FORMAT @@ -185,23 +184,21 @@ def test_is_valid_bittensor_address_or_public_key(mocker, test_input, expected_r ("hotkey", "unlock_hotkey"), ], ) -def test_unlock_key(mocker, unlock_type, wallet_method): +def test_unlock_key(fake_wallet, unlock_type, wallet_method): """Test the unlock key function.""" - # Preps - mock_wallet = mocker.Mock(autospec=Wallet) # Call - result = utils.unlock_key(mock_wallet, unlock_type=unlock_type) + result = utils.unlock_key(fake_wallet, unlock_type=unlock_type) # Asserts - getattr(mock_wallet, wallet_method).assert_called_once() + getattr(fake_wallet, wallet_method).assert_called_once() assert result == utils.UnlockStatus(True, "") -def test_unlock_key_raise_value_error(mocker): +def test_unlock_key_raise_value_error(fake_wallet): """Test the unlock key function raises ValueError.""" with pytest.raises(ValueError): - utils.unlock_key(wallet=mocker.Mock(autospec=Wallet), unlock_type="coldkeypub") + utils.unlock_key(wallet=fake_wallet, unlock_type="coldkeypub") @pytest.mark.parametrize( @@ -223,11 +220,10 @@ def test_unlock_key_raise_value_error(mocker): ], ids=["PasswordError", "KeyFileError"], ) -def test_unlock_key_errors(mocker, side_effect, response): +def test_unlock_key_errors(fake_wallet, side_effect, response): """Test the unlock key function handles the errors.""" - mock_wallet = mocker.Mock(autospec=Wallet) - mock_wallet.unlock_coldkey.side_effect = side_effect - result = utils.unlock_key(wallet=mock_wallet) + fake_wallet.unlock_coldkey.side_effect = side_effect + result = utils.unlock_key(wallet=fake_wallet) assert result == response