diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 867c4d062e..23f23f5a6e 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -114,6 +114,7 @@ add_stake_extrinsic, add_stake_multiple_extrinsic, set_auto_stake_extrinsic, + subnet_buyback_extrinsic, ) from bittensor.core.extrinsics.asyncex.start_call import start_call_extrinsic from bittensor.core.extrinsics.asyncex.take import set_take_extrinsic @@ -9125,6 +9126,64 @@ async def start_call( wait_for_revealed_execution=wait_for_revealed_execution, ) + async def subnet_buyback( + self, + wallet: "Wallet", + netuid: int, + hotkey_ss58: str, + amount: Balance, + limit_price: Optional[Balance] = None, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Executes a subnet buyback by staking TAO and immediately burning the resulting Alpha. + + Only the subnet owner can call this method, and it is rate-limited to one call per subnet tempo. + + Parameters: + wallet: The wallet used to sign the extrinsic (must be the subnet owner). + netuid: The unique identifier of the subnet. + hotkey_ss58: The `SS58` address of the hotkey account to stake to. + amount: The amount of TAO to use for the buyback. + limit_price: Optional limit price expressed in units of RAO per one Alpha. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + check_balance_amount(amount) + if limit_price is not None: + check_balance_amount(limit_price) + return await subnet_buyback_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + hotkey_ss58=hotkey_ss58, + amount=amount, + limit_price=limit_price, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + async def swap_stake( self, wallet: "Wallet", diff --git a/bittensor/core/extrinsics/asyncex/staking.py b/bittensor/core/extrinsics/asyncex/staking.py index 1ff60d8dae..c375527882 100644 --- a/bittensor/core/extrinsics/asyncex/staking.py +++ b/bittensor/core/extrinsics/asyncex/staking.py @@ -452,6 +452,189 @@ async def add_stake_multiple_extrinsic( return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) +async def subnet_buyback_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + hotkey_ss58: str, + amount: Balance, + limit_price: Optional[Balance] = None, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, +) -> ExtrinsicResponse: + """ + Executes a subnet buyback by staking TAO and immediately burning the resulting Alpha. + + Parameters: + subtensor: Subtensor instance with the connection to the chain. + wallet: Bittensor wallet object. + netuid: The unique identifier of the subnet. + hotkey_ss58: The `ss58` address of the hotkey account to stake to. + amount: Amount to stake as Bittensor balance in TAO always. + limit_price: Optional limit price expressed in units of RAO per one Alpha. + mev_protection: If True, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If False, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You can + think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Raises: + SubstrateRequestException: Raised if the extrinsic fails to be included in the block within the timeout. + + Notes: + The `data` field in the returned `ExtrinsicResponse` contains extra information about the extrinsic execution. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + if not isinstance(amount, Balance): + raise BalanceTypeError("`amount` must be an instance of Balance.") + + if limit_price is not None and not isinstance(limit_price, Balance): + raise BalanceTypeError("`limit_price` must be an instance of Balance.") + + old_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) + block_hash = await subtensor.substrate.get_chain_head() + + # Get current stake and existential deposit + old_stake, existential_deposit = await asyncio.gather( + subtensor.get_stake( + hotkey_ss58=hotkey_ss58, + coldkey_ss58=wallet.coldkeypub.ss58_address, + netuid=netuid, + block_hash=block_hash, + ), + subtensor.get_existential_deposit(block_hash=block_hash), + ) + + # Leave existential balance to keep key alive. + if old_balance <= existential_deposit: + return ExtrinsicResponse( + False, + f"Balance ({old_balance}) is not enough to cover existential deposit `{existential_deposit}`.", + ).with_log() + + # Leave existential balance to keep key alive. + if amount > old_balance - existential_deposit: + # If we are staking all, we need to leave at least the existential deposit. + amount = old_balance - existential_deposit + + # Check enough to stake. + if amount > old_balance: + message = "Not enough stake" + logging.debug(f":cross_mark: [red]{message}:[/red]") + logging.debug(f"\t\tbalance:{old_balance}") + logging.debug(f"\t\tamount: {amount}") + logging.debug(f"\t\twallet: {wallet.name}") + return ExtrinsicResponse(False, f"{message}.").with_log() + + if limit_price is None: + logging.debug( + f"Subnet buyback on: [blue]netuid: [green]{netuid}[/green], amount: [green]{amount}[/green], " + f"hotkey: [green]{hotkey_ss58}[/green] on [blue]{subtensor.network}[/blue]." + ) + else: + logging.debug( + f"Subnet buyback with limit: [blue]netuid: [green]{netuid}[/green], " + f"amount: [green]{amount}[/green], " + f"limit price: [green]{limit_price}[/green], " + f"hotkey: [green]{hotkey_ss58}[/green] on [blue]{subtensor.network}[/blue]." + ) + + call = await SubtensorModule(subtensor).subnet_buyback( + netuid=netuid, + hotkey=hotkey_ss58, + amount=amount.rao, + limit=None if limit_price is None else limit_price.rao, + ) + + block_hash_before = await subtensor.get_block_hash() + if mev_protection: + response = await submit_encrypted_extrinsic( + subtensor=subtensor, + wallet=wallet, + call=call, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + else: + response = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + nonce_key="coldkeypub", + use_nonce=True, + period=period, + raise_error=raise_error, + ) + if response.success: + sim_swap = await subtensor.sim_swap( + origin_netuid=0, + destination_netuid=netuid, + amount=amount, + block_hash=block_hash_before, + ) + response.transaction_tao_fee = sim_swap.tao_fee + response.transaction_alpha_fee = sim_swap.alpha_fee.set_unit(netuid) + + if not wait_for_finalization and not wait_for_inclusion: + return response + logging.debug("[green]Finalized.[/green]") + + new_block_hash = await subtensor.substrate.get_chain_head() + new_balance, new_stake = await asyncio.gather( + subtensor.get_balance( + wallet.coldkeypub.ss58_address, block_hash=new_block_hash + ), + subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + netuid=netuid, + block_hash=new_block_hash, + ), + ) + + logging.debug( + f"Balance: [blue]{old_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" + ) + logging.debug( + f"Stake: [blue]{old_stake}[/blue] :arrow_right: [green]{new_stake}[/green]" + ) + response.data = { + "balance_before": old_balance, + "balance_after": new_balance, + "stake_before": old_stake, + "stake_after": new_stake, + } + return response + + logging.error(f"[red]{response.message}[/red]") + return response + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + async def set_auto_stake_extrinsic( subtensor: "AsyncSubtensor", wallet: "Wallet", diff --git a/bittensor/core/extrinsics/pallets/subtensor_module.py b/bittensor/core/extrinsics/pallets/subtensor_module.py index fd8fa64220..099c5a477d 100644 --- a/bittensor/core/extrinsics/pallets/subtensor_module.py +++ b/bittensor/core/extrinsics/pallets/subtensor_module.py @@ -1,8 +1,8 @@ from dataclasses import dataclass from typing import Literal, Optional -from bittensor.utils import deprecated_message + from bittensor.core.types import Salt, UIDs, Weights -from bittensor.utils import Certificate +from bittensor.utils import Certificate, deprecated_message from .base import Call from .base import CallBuilder as _BasePallet @@ -663,6 +663,31 @@ def start_call(self, netuid: int) -> Call: """ return self.create_composed_call(netuid=netuid) + def subnet_buyback( + self, + netuid: int, + hotkey: str, + amount: int, + limit: Optional[int] = None, + ) -> Call: + """Returns GenericCall instance for Subtensor function SubtensorModule.subnet_buyback. + + Parameters: + netuid: The netuid of the subnet to buy back on. + hotkey: The hotkey SS58 address associated with the buyback. + amount: Amount of TAO in RAO to use for the buyback. + limit: Optional limit price expressed in units of RAO per one Alpha. + + Returns: + GenericCall instance. + """ + return self.create_composed_call( + netuid=netuid, + hotkey=hotkey, + amount=amount, + limit=limit, + ) + def swap_stake( self, hotkey: str, diff --git a/bittensor/core/extrinsics/staking.py b/bittensor/core/extrinsics/staking.py index a8f3d4c913..ee28d12055 100644 --- a/bittensor/core/extrinsics/staking.py +++ b/bittensor/core/extrinsics/staking.py @@ -443,6 +443,185 @@ def add_stake_multiple_extrinsic( return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) +def subnet_buyback_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + hotkey_ss58: str, + amount: Balance, + limit_price: Optional[Balance] = None, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, +) -> ExtrinsicResponse: + """ + Executes a subnet buyback by staking TAO and immediately burning the resulting Alpha. + + Parameters: + subtensor: Subtensor instance with the connection to the chain. + wallet: Bittensor wallet object. + netuid: The unique identifier of the subnet. + hotkey_ss58: The `ss58` address of the hotkey account to stake to. + amount: Amount to stake as Bittensor balance in TAO always. + limit_price: Optional limit price expressed in units of RAO per one Alpha. + mev_protection: If True, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If False, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You can + think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Raises: + SubstrateRequestException: Raised if the extrinsic fails to be included in the block within the timeout. + + Notes: + The `data` field in the returned `ExtrinsicResponse` contains extra information about the extrinsic execution. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + if not isinstance(amount, Balance): + raise BalanceTypeError("`amount` must be an instance of Balance.") + + if limit_price is not None and not isinstance(limit_price, Balance): + raise BalanceTypeError("`limit_price` must be an instance of Balance.") + + old_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) + block = subtensor.get_current_block() + + # Get current stake and existential deposit + old_stake = subtensor.get_stake( + hotkey_ss58=hotkey_ss58, + coldkey_ss58=wallet.coldkeypub.ss58_address, + netuid=netuid, + block=block, + ) + existential_deposit = subtensor.get_existential_deposit(block=block) + + # Leave existential balance to keep key alive. + if old_balance <= existential_deposit: + return ExtrinsicResponse( + False, + f"Balance ({old_balance}) is not enough to cover existential deposit `{existential_deposit}`.", + ).with_log() + + # Leave existential balance to keep key alive. + if amount > old_balance - existential_deposit: + # If we are staking all, we need to leave at least the existential deposit. + amount = old_balance - existential_deposit + + # Check enough to stake. + if amount > old_balance: + message = "Not enough stake" + logging.debug(f":cross_mark: [red]{message}:[/red]") + logging.debug(f"\t\tbalance:{old_balance}") + logging.debug(f"\t\tamount: {amount}") + logging.debug(f"\t\twallet: {wallet.name}") + return ExtrinsicResponse(False, f"{message}.").with_log() + + if limit_price is None: + logging.debug( + f"Subnet buyback on: [blue]netuid: [green]{netuid}[/green], amount: [green]{amount}[/green], " + f"hotkey: [green]{hotkey_ss58}[/green] on [blue]{subtensor.network}[/blue]." + ) + else: + logging.debug( + f"Subnet buyback with limit: [blue]netuid: [green]{netuid}[/green], " + f"amount: [green]{amount}[/green], " + f"limit price: [green]{limit_price}[/green], " + f"hotkey: [green]{hotkey_ss58}[/green] on [blue]{subtensor.network}[/blue]." + ) + + call = SubtensorModule(subtensor).subnet_buyback( + netuid=netuid, + hotkey=hotkey_ss58, + amount=amount.rao, + limit=None if limit_price is None else limit_price.rao, + ) + + block_before = subtensor.block + if mev_protection: + response = submit_encrypted_extrinsic( + subtensor=subtensor, + wallet=wallet, + call=call, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + else: + response = subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + use_nonce=True, + nonce_key="coldkeypub", + period=period, + raise_error=raise_error, + ) + if response.success: + sim_swap = subtensor.sim_swap( + origin_netuid=0, + destination_netuid=netuid, + amount=amount, + block=block_before, + ) + response.transaction_tao_fee = sim_swap.tao_fee + response.transaction_alpha_fee = sim_swap.alpha_fee.set_unit(netuid) + + if not wait_for_finalization and not wait_for_inclusion: + return response + logging.debug("[green]Finalized.[/green]") + + new_block = subtensor.get_current_block() + new_balance = subtensor.get_balance( + wallet.coldkeypub.ss58_address, block=new_block + ) + new_stake = subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + netuid=netuid, + block=new_block, + ) + + logging.debug( + f"Balance: [blue]{old_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" + ) + logging.debug( + f"Stake: [blue]{old_stake}[/blue] :arrow_right: [green]{new_stake}[/green]" + ) + response.data = { + "balance_before": old_balance, + "balance_after": new_balance, + "stake_before": old_stake, + "stake_after": new_stake, + } + return response + + logging.error(f"[red]{response.message}[/red]") + return response + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + def set_auto_stake_extrinsic( subtensor: "Subtensor", wallet: "Wallet", diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index dce73a20c6..2181046d71 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -113,6 +113,7 @@ add_stake_extrinsic, add_stake_multiple_extrinsic, set_auto_stake_extrinsic, + subnet_buyback_extrinsic, ) from bittensor.core.extrinsics.start_call import start_call_extrinsic from bittensor.core.extrinsics.take import set_take_extrinsic @@ -7833,6 +7834,64 @@ def start_call( wait_for_revealed_execution=wait_for_revealed_execution, ) + def subnet_buyback( + self, + wallet: "Wallet", + netuid: int, + hotkey_ss58: str, + amount: Balance, + limit_price: Optional[Balance] = None, + *, + mev_protection: bool = DEFAULT_MEV_PROTECTION, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + wait_for_revealed_execution: bool = True, + ) -> ExtrinsicResponse: + """ + Executes a subnet buyback by staking TAO and immediately burning the resulting Alpha. + + Only the subnet owner can call this method, and it is rate-limited to one call per subnet tempo. + + Parameters: + wallet: The wallet used to sign the extrinsic (must be the subnet owner). + netuid: The unique identifier of the subnet. + hotkey_ss58: The `SS58` address of the hotkey account to stake to. + amount: The amount of TAO to use for the buyback. + limit_price: Optional limit price expressed in units of RAO per one Alpha. + mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators + decrypt and execute it. If `False`, submits the transaction directly without encryption. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + check_balance_amount(amount) + if limit_price is not None: + check_balance_amount(limit_price) + return subnet_buyback_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + hotkey_ss58=hotkey_ss58, + amount=amount, + limit_price=limit_price, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + def swap_stake( self, wallet: "Wallet", diff --git a/bittensor/extras/dev_framework/calls/non_sudo_calls.py b/bittensor/extras/dev_framework/calls/non_sudo_calls.py index 1606be4b5e..04c898524f 100644 --- a/bittensor/extras/dev_framework/calls/non_sudo_calls.py +++ b/bittensor/extras/dev_framework/calls/non_sudo_calls.py @@ -11,7 +11,7 @@ Note: Any manual changes will be overwritten the next time the generator is run. - Subtensor spec version: 365 + Subtensor spec version: 375 """ from collections import namedtuple @@ -42,6 +42,9 @@ ANNOUNCE = namedtuple( "ANNOUNCE", ["wallet", "pallet", "real", "call_hash"] ) # args: [real: AccountIdLookupOf, call_hash: CallHashOf] | Pallet: Proxy +ANNOUNCE_COLDKEY_SWAP = namedtuple( + "ANNOUNCE_COLDKEY_SWAP", ["wallet", "pallet", "new_coldkey_hash"] +) # args: [new_coldkey_hash: T::Hash] | Pallet: SubtensorModule ANNOUNCE_NEXT_KEY = namedtuple( "ANNOUNCE_NEXT_KEY", ["wallet", "pallet", "public_key"] ) # args: [public_key: BoundedVec>] | Pallet: MevShield @@ -256,6 +259,9 @@ "pallet", ], ) # args: [] | Pallet: Swap +DISABLE_VOTING_POWER_TRACKING = namedtuple( + "DISABLE_VOTING_POWER_TRACKING", ["wallet", "pallet", "netuid"] +) # args: [netuid: NetUid] | Pallet: SubtensorModule DISABLE_WHITELIST = namedtuple( "DISABLE_WHITELIST", ["wallet", "pallet", "disabled"] ) # args: [disabled: bool] | Pallet: EVM @@ -265,12 +271,22 @@ DISPATCH_AS_FALLIBLE = namedtuple( "DISPATCH_AS_FALLIBLE", ["wallet", "pallet", "as_origin", "call"] ) # args: [as_origin: Box, call: Box<::RuntimeCall>] | Pallet: Utility +DISPUTE_COLDKEY_SWAP = namedtuple( + "DISPUTE_COLDKEY_SWAP", + [ + "wallet", + "pallet", + ], +) # args: [] | Pallet: SubtensorModule DISSOLVE = namedtuple( "DISSOLVE", ["wallet", "pallet", "crowdloan_id"] ) # args: [crowdloan_id: CrowdloanId] | Pallet: Crowdloan DISSOLVE_NETWORK = namedtuple( "DISSOLVE_NETWORK", ["wallet", "pallet", "coldkey", "netuid"] ) # args: [coldkey: T::AccountId, netuid: NetUid] | Pallet: SubtensorModule +ENABLE_VOTING_POWER_TRACKING = namedtuple( + "ENABLE_VOTING_POWER_TRACKING", ["wallet", "pallet", "netuid"] +) # args: [netuid: NetUid] | Pallet: SubtensorModule ENSURE_UPDATED = namedtuple( "ENSURE_UPDATED", ["wallet", "pallet", "hashes"] ) # args: [hashes: Vec] | Pallet: Preimage @@ -543,6 +559,9 @@ REQUEST_PREIMAGE = namedtuple( "REQUEST_PREIMAGE", ["wallet", "pallet", "hash"] ) # args: [hash: T::Hash] | Pallet: Preimage +RESET_COLDKEY_SWAP = namedtuple( + "RESET_COLDKEY_SWAP", ["wallet", "pallet", "coldkey"] +) # args: [coldkey: T::AccountId] | Pallet: SubtensorModule REVEAL_MECHANISM_WEIGHTS = namedtuple( "REVEAL_MECHANISM_WEIGHTS", ["wallet", "pallet", "netuid", "mecid", "uids", "values", "salt", "version_key"], @@ -726,6 +745,9 @@ SUBMIT_ENCRYPTED = namedtuple( "SUBMIT_ENCRYPTED", ["wallet", "pallet", "commitment", "ciphertext"] ) # args: [commitment: T::Hash, ciphertext: BoundedVec>] | Pallet: MevShield +SUBNET_BUYBACK = namedtuple( + "SUBNET_BUYBACK", ["wallet", "pallet", "hotkey", "netuid", "amount", "limit"] +) # args: [hotkey: T::AccountId, netuid: NetUid, amount: TaoCurrency, limit: Option] | Pallet: SubtensorModule SUDO = namedtuple( "SUDO", ["wallet", "pallet", "call"] ) # args: [call: Box<::RuntimeCall>] | Pallet: Sudo @@ -735,6 +757,9 @@ SWAP_COLDKEY = namedtuple( "SWAP_COLDKEY", ["wallet", "pallet", "old_coldkey", "new_coldkey", "swap_cost"] ) # args: [old_coldkey: T::AccountId, new_coldkey: T::AccountId, swap_cost: TaoCurrency] | Pallet: SubtensorModule +SWAP_COLDKEY_ANNOUNCED = namedtuple( + "SWAP_COLDKEY_ANNOUNCED", ["wallet", "pallet", "new_coldkey"] +) # args: [new_coldkey: T::AccountId] | Pallet: SubtensorModule SWAP_HOTKEY = namedtuple( "SWAP_HOTKEY", ["wallet", "pallet", "hotkey", "new_hotkey", "netuid"] ) # args: [hotkey: T::AccountId, new_hotkey: T::AccountId, netuid: Option] | Pallet: SubtensorModule diff --git a/bittensor/extras/dev_framework/calls/pallets.py b/bittensor/extras/dev_framework/calls/pallets.py index feeb55559a..abba174a69 100644 --- a/bittensor/extras/dev_framework/calls/pallets.py +++ b/bittensor/extras/dev_framework/calls/pallets.py @@ -1,5 +1,5 @@ """ " -Subtensor spec version: 365 +Subtensor spec version: 375 """ System = "System" diff --git a/bittensor/extras/dev_framework/calls/sudo_calls.py b/bittensor/extras/dev_framework/calls/sudo_calls.py index 693a6b1e2a..452eb60940 100644 --- a/bittensor/extras/dev_framework/calls/sudo_calls.py +++ b/bittensor/extras/dev_framework/calls/sudo_calls.py @@ -11,7 +11,7 @@ Note: Any manual changes will be overwritten the next time the generator is run. - Subtensor spec version: 365 + Subtensor spec version: 375 """ from collections import namedtuple @@ -56,8 +56,12 @@ SUDO_SET_CK_BURN = namedtuple( "SUDO_SET_CK_BURN", ["wallet", "pallet", "sudo", "burn"] ) # args: [burn: u64] | Pallet: AdminUtils -SUDO_SET_COLDKEY_SWAP_SCHEDULE_DURATION = namedtuple( - "SUDO_SET_COLDKEY_SWAP_SCHEDULE_DURATION", ["wallet", "pallet", "sudo", "duration"] +SUDO_SET_COLDKEY_SWAP_ANNOUNCEMENT_DELAY = namedtuple( + "SUDO_SET_COLDKEY_SWAP_ANNOUNCEMENT_DELAY", ["wallet", "pallet", "sudo", "duration"] +) # args: [duration: BlockNumberFor] | Pallet: AdminUtils +SUDO_SET_COLDKEY_SWAP_REANNOUNCEMENT_DELAY = namedtuple( + "SUDO_SET_COLDKEY_SWAP_REANNOUNCEMENT_DELAY", + ["wallet", "pallet", "sudo", "duration"], ) # args: [duration: BlockNumberFor] | Pallet: AdminUtils SUDO_SET_COMMIT_REVEAL_VERSION = namedtuple( "SUDO_SET_COMMIT_REVEAL_VERSION", ["wallet", "pallet", "sudo", "version"] @@ -117,6 +121,9 @@ SUDO_SET_MAX_DIFFICULTY = namedtuple( "SUDO_SET_MAX_DIFFICULTY", ["wallet", "pallet", "sudo", "netuid", "max_difficulty"] ) # args: [netuid: NetUid, max_difficulty: u64] | Pallet: AdminUtils +SUDO_SET_MAX_MECHANISM_COUNT = namedtuple( + "SUDO_SET_MAX_MECHANISM_COUNT", ["wallet", "pallet", "sudo", "max_mechanism_count"] +) # args: [max_mechanism_count: MechId] | Pallet: AdminUtils SUDO_SET_MAX_REGISTRATIONS_PER_BLOCK = namedtuple( "SUDO_SET_MAX_REGISTRATIONS_PER_BLOCK", ["wallet", "pallet", "sudo", "netuid", "max_registrations_per_block"], @@ -258,6 +265,9 @@ SUDO_SET_TX_RATE_LIMIT = namedtuple( "SUDO_SET_TX_RATE_LIMIT", ["wallet", "pallet", "sudo", "tx_rate_limit"] ) # args: [tx_rate_limit: u64] | Pallet: AdminUtils +SUDO_SET_VOTING_POWER_EMA_ALPHA = namedtuple( + "SUDO_SET_VOTING_POWER_EMA_ALPHA", ["wallet", "pallet", "sudo", "netuid", "alpha"] +) # args: [netuid: NetUid, alpha: u64] | Pallet: SubtensorModule SUDO_SET_WEIGHTS_SET_RATE_LIMIT = namedtuple( "SUDO_SET_WEIGHTS_SET_RATE_LIMIT", ["wallet", "pallet", "sudo", "netuid", "weights_set_rate_limit"], diff --git a/bittensor/extras/subtensor_api/extrinsics.py b/bittensor/extras/subtensor_api/extrinsics.py index e67dcb93ca..b1bb000336 100644 --- a/bittensor/extras/subtensor_api/extrinsics.py +++ b/bittensor/extras/subtensor_api/extrinsics.py @@ -39,6 +39,7 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.set_commitment = subtensor.set_commitment self.set_root_claim_type = subtensor.set_root_claim_type self.start_call = subtensor.start_call + self.subnet_buyback = subtensor.subnet_buyback self.swap_coldkey_announced = subtensor.swap_coldkey_announced self.swap_stake = subtensor.swap_stake self.toggle_user_liquidity = subtensor.toggle_user_liquidity diff --git a/bittensor/extras/subtensor_api/staking.py b/bittensor/extras/subtensor_api/staking.py index 16d7b6cf5a..7bc26f1f6f 100644 --- a/bittensor/extras/subtensor_api/staking.py +++ b/bittensor/extras/subtensor_api/staking.py @@ -35,6 +35,7 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.set_auto_stake = subtensor.set_auto_stake self.set_root_claim_type = subtensor.set_root_claim_type self.sim_swap = subtensor.sim_swap + self.subnet_buyback = subtensor.subnet_buyback self.swap_stake = subtensor.swap_stake self.transfer_stake = subtensor.transfer_stake self.unstake = subtensor.unstake diff --git a/tests/e2e_tests/test_commit_reveal.py b/tests/e2e_tests/test_commit_reveal.py index 6d21c8d46f..ca79eb3a28 100644 --- a/tests/e2e_tests/test_commit_reveal.py +++ b/tests/e2e_tests/test_commit_reveal.py @@ -12,6 +12,7 @@ REGISTER_SUBNET, SUDO_SET_ADMIN_FREEZE_WINDOW, SUDO_SET_TEMPO, + SUDO_SET_MAX_ALLOWED_UIDS, SUDO_SET_MECHANISM_COUNT, SUDO_SET_COMMIT_REVEAL_WEIGHTS_ENABLED, SUDO_SET_WEIGHTS_SET_RATE_LIMIT, @@ -49,6 +50,9 @@ def test_commit_and_reveal_weights_cr4(subtensor, alice_wallet): steps = [ SUDO_SET_ADMIN_FREEZE_WINDOW(alice_wallet, AdminUtils, True, 0), REGISTER_SUBNET(alice_wallet), + SUDO_SET_MAX_ALLOWED_UIDS( + alice_wallet, AdminUtils, True, NETUID, int(256 / TESTED_MECHANISMS) + ), SUDO_SET_MECHANISM_COUNT( alice_wallet, AdminUtils, True, NETUID, TESTED_MECHANISMS ), @@ -121,6 +125,7 @@ def test_commit_and_reveal_weights_cr4(subtensor, alice_wallet): wait_for_finalization=True, block_time=BLOCK_TIME, period=16, + raise_error=True, ) # Assert committing was a success @@ -236,6 +241,9 @@ async def test_commit_and_reveal_weights_cr4_async(async_subtensor, alice_wallet steps = [ SUDO_SET_ADMIN_FREEZE_WINDOW(alice_wallet, AdminUtils, True, 0), REGISTER_SUBNET(alice_wallet), + SUDO_SET_MAX_ALLOWED_UIDS( + alice_wallet, AdminUtils, True, NETUID, int(256 / TESTED_MECHANISMS) + ), SUDO_SET_MECHANISM_COUNT( alice_wallet, AdminUtils, True, NETUID, TESTED_MECHANISMS ), diff --git a/tests/e2e_tests/test_commit_weights.py b/tests/e2e_tests/test_commit_weights.py index 0d488eedf0..536c8c86af 100644 --- a/tests/e2e_tests/test_commit_weights.py +++ b/tests/e2e_tests/test_commit_weights.py @@ -15,13 +15,14 @@ NETUID, SUDO_SET_ADMIN_FREEZE_WINDOW, SUDO_SET_TEMPO, + SUDO_SET_MAX_ALLOWED_UIDS, SUDO_SET_MECHANISM_COUNT, SUDO_SET_COMMIT_REVEAL_WEIGHTS_ENABLED, SUDO_SET_WEIGHTS_SET_RATE_LIMIT, AdminUtils, ) -TESTED_SUB_SUBNETS = 2 +TESTED_MECHANISMS = 2 def test_commit_and_reveal_weights_legacy(subtensor, alice_wallet): @@ -44,8 +45,11 @@ def test_commit_and_reveal_weights_legacy(subtensor, alice_wallet): steps = [ SUDO_SET_ADMIN_FREEZE_WINDOW(alice_wallet, AdminUtils, True, 0), REGISTER_SUBNET(alice_wallet), + SUDO_SET_MAX_ALLOWED_UIDS( + alice_wallet, AdminUtils, True, NETUID, int(256 / TESTED_MECHANISMS) + ), SUDO_SET_MECHANISM_COUNT( - alice_wallet, AdminUtils, True, NETUID, TESTED_SUB_SUBNETS + alice_wallet, AdminUtils, True, NETUID, TESTED_MECHANISMS ), ACTIVATE_SUBNET(alice_wallet), SUDO_SET_TEMPO(alice_wallet, AdminUtils, True, NETUID, TEMPO_TO_SET), @@ -77,7 +81,7 @@ def test_commit_and_reveal_weights_legacy(subtensor, alice_wallet): assert response.success, response.message assert subtensor.subnets.weights_rate_limit(netuid=alice_sn.netuid) == 0 - for mechid in range(TESTED_SUB_SUBNETS): + for mechid in range(TESTED_MECHANISMS): logging.console.info( f"[magenta]Testing subnet mechanism {alice_sn.netuid}.{mechid}[/magenta]" ) @@ -174,7 +178,7 @@ async def test_commit_and_reveal_weights_legacy_async(async_subtensor, alice_wal SUDO_SET_ADMIN_FREEZE_WINDOW(alice_wallet, AdminUtils, True, 0), REGISTER_SUBNET(alice_wallet), SUDO_SET_MECHANISM_COUNT( - alice_wallet, AdminUtils, True, NETUID, TESTED_SUB_SUBNETS + alice_wallet, AdminUtils, True, NETUID, TESTED_MECHANISMS ), ACTIVATE_SUBNET(alice_wallet), SUDO_SET_TEMPO(alice_wallet, AdminUtils, True, NETUID, TEMPO_TO_SET), @@ -429,6 +433,9 @@ async def test_commit_weights_uses_next_nonce_async(async_subtensor, alice_walle steps = [ SUDO_SET_ADMIN_FREEZE_WINDOW(alice_wallet, AdminUtils, True, 0), REGISTER_SUBNET(alice_wallet), + SUDO_SET_MAX_ALLOWED_UIDS( + alice_wallet, AdminUtils, True, NETUID, int(256 / TESTED_MECHANISMS) + ), ACTIVATE_SUBNET(alice_wallet), SUDO_SET_TEMPO(alice_wallet, AdminUtils, True, NETUID, TEMPO_TO_SET), SUDO_SET_COMMIT_REVEAL_WEIGHTS_ENABLED( diff --git a/tests/e2e_tests/test_set_weights.py b/tests/e2e_tests/test_set_weights.py index 0f6aa9240d..8925f0e619 100644 --- a/tests/e2e_tests/test_set_weights.py +++ b/tests/e2e_tests/test_set_weights.py @@ -17,6 +17,7 @@ SUDO_SET_ADMIN_FREEZE_WINDOW, SUDO_SET_COMMIT_REVEAL_WEIGHTS_ENABLED, SUDO_SET_LOCK_REDUCTION_INTERVAL, + SUDO_SET_MAX_ALLOWED_UIDS, SUDO_SET_MECHANISM_COUNT, SUDO_SET_NETWORK_RATE_LIMIT, SUDO_SET_TEMPO, @@ -57,6 +58,9 @@ def test_set_weights_uses_next_nonce(subtensor, alice_wallet): sns_steps = [ REGISTER_SUBNET(alice_wallet), + SUDO_SET_MAX_ALLOWED_UIDS( + alice_wallet, AdminUtils, True, NETUID, int(256 / TESTED_MECHANISMS) + ), SUDO_SET_TEMPO(alice_wallet, AdminUtils, True, NETUID, subnet_tempo), SUDO_SET_MECHANISM_COUNT( alice_wallet, AdminUtils, True, NETUID, TESTED_MECHANISMS @@ -199,6 +203,9 @@ async def test_set_weights_uses_next_nonce_async(async_subtensor, alice_wallet): sns_steps = [ REGISTER_SUBNET(alice_wallet), + SUDO_SET_MAX_ALLOWED_UIDS( + alice_wallet, AdminUtils, True, NETUID, int(256 / TESTED_MECHANISMS) + ), SUDO_SET_TEMPO(alice_wallet, AdminUtils, True, NETUID, subnet_tempo), SUDO_SET_MECHANISM_COUNT( alice_wallet, AdminUtils, True, NETUID, TESTED_MECHANISMS diff --git a/tests/e2e_tests/test_subnet_buyback.py b/tests/e2e_tests/test_subnet_buyback.py new file mode 100644 index 0000000000..6ef853a864 --- /dev/null +++ b/tests/e2e_tests/test_subnet_buyback.py @@ -0,0 +1,231 @@ +import pytest + +from bittensor.utils.balance import Balance +from tests.e2e_tests.utils import ( + ACTIVATE_SUBNET, + REGISTER_NEURON, + REGISTER_SUBNET, + TestSubnet, +) + + +def test_subnet_buyback(subtensor, alice_wallet, bob_wallet): + """Tests subnet buyback without limit price. + + Steps: + - Create subnet and register neuron for the target hotkey + - Verify no stake before buyback + - Execute subnet buyback as subnet owner + - Confirm stake is burned and coldkey balance decreases + """ + alice_sn = TestSubnet(subtensor) + steps = [ + REGISTER_SUBNET(alice_wallet), + ACTIVATE_SUBNET(alice_wallet), + REGISTER_NEURON(bob_wallet), + ] + alice_sn.execute_steps(steps) + + # no stake before buyback + stake_before = subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_sn.netuid, + ) + assert stake_before == Balance(0).set_unit(alice_sn.netuid) + + # track coldkey balance before buyback + balance_before = subtensor.wallets.get_balance( + address=alice_wallet.coldkeypub.ss58_address + ) + + response = subtensor.staking.subnet_buyback( + wallet=alice_wallet, + netuid=alice_sn.netuid, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + amount=Balance.from_tao(10), + period=16, + ) + assert response.success, response.message + + # stake is burned immediately after buyback + stake_after = subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_sn.netuid, + ) + assert stake_after == Balance(0).set_unit(alice_sn.netuid) + + # buyback spends TAO from the subnet owner coldkey + balance_after = subtensor.wallets.get_balance( + address=alice_wallet.coldkeypub.ss58_address + ) + assert balance_after < balance_before + + +@pytest.mark.asyncio +async def test_subnet_buyback_async(async_subtensor, alice_wallet, bob_wallet): + """Tests subnet buyback without limit price (async). + + Steps: + - Create subnet and register neuron for the target hotkey + - Verify no stake before buyback + - Execute subnet buyback as subnet owner + - Confirm stake is burned and coldkey balance decreases + """ + alice_sn = TestSubnet(async_subtensor) + steps = [ + REGISTER_SUBNET(alice_wallet), + ACTIVATE_SUBNET(alice_wallet), + REGISTER_NEURON(bob_wallet), + ] + await alice_sn.async_execute_steps(steps) + + # no stake before buyback + stake_before = await async_subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_sn.netuid, + ) + assert stake_before == Balance(0).set_unit(alice_sn.netuid) + + # track coldkey balance before buyback + balance_before = await async_subtensor.wallets.get_balance( + address=alice_wallet.coldkeypub.ss58_address + ) + + response = await async_subtensor.staking.subnet_buyback( + wallet=alice_wallet, + netuid=alice_sn.netuid, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + amount=Balance.from_tao(10), + period=16, + ) + assert response.success, response.message + + # stake is burned immediately after buyback + stake_after = await async_subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_sn.netuid, + ) + assert stake_after == Balance(0).set_unit(alice_sn.netuid) + + # buyback spends TAO from the subnet owner coldkey + balance_after = await async_subtensor.wallets.get_balance( + address=alice_wallet.coldkeypub.ss58_address + ) + assert balance_after < balance_before + + +def test_subnet_buyback_with_limit_price(subtensor, alice_wallet, bob_wallet): + """Tests subnet buyback with limit price. + + Steps: + - Create subnet and register neuron for the target hotkey + - Verify no stake before buyback + - Execute subnet buyback with limit price as subnet owner + - Confirm stake is burned and coldkey balance decreases + """ + alice_sn = TestSubnet(subtensor) + steps = [ + REGISTER_SUBNET(alice_wallet), + ACTIVATE_SUBNET(alice_wallet), + REGISTER_NEURON(bob_wallet), + ] + alice_sn.execute_steps(steps) + + # no stake before buyback + stake_before = subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_sn.netuid, + ) + assert stake_before == Balance(0).set_unit(alice_sn.netuid) + + # track coldkey balance before buyback + balance_before = subtensor.wallets.get_balance( + address=alice_wallet.coldkeypub.ss58_address + ) + + response = subtensor.staking.subnet_buyback( + wallet=alice_wallet, + netuid=alice_sn.netuid, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + amount=Balance.from_tao(10), + limit_price=Balance.from_tao(2), + period=16, + ) + assert response.success, response.message + + # stake is burned immediately after buyback + stake_after = subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_sn.netuid, + ) + assert stake_after == Balance(0).set_unit(alice_sn.netuid) + + # buyback spends TAO from the subnet owner coldkey + balance_after = subtensor.wallets.get_balance( + address=alice_wallet.coldkeypub.ss58_address + ) + assert balance_after < balance_before + + +@pytest.mark.asyncio +async def test_subnet_buyback_with_limit_price_async( + async_subtensor, alice_wallet, bob_wallet +): + """Tests subnet buyback with limit price (async). + + Steps: + - Create subnet and register neuron for the target hotkey + - Verify no stake before buyback + - Execute subnet buyback with limit price as subnet owner + - Confirm stake is burned and coldkey balance decreases + """ + alice_sn = TestSubnet(async_subtensor) + steps = [ + REGISTER_SUBNET(alice_wallet), + ACTIVATE_SUBNET(alice_wallet), + REGISTER_NEURON(bob_wallet), + ] + await alice_sn.async_execute_steps(steps) + + # no stake before buyback + stake_before = await async_subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_sn.netuid, + ) + assert stake_before == Balance(0).set_unit(alice_sn.netuid) + + # track coldkey balance before buyback + balance_before = await async_subtensor.wallets.get_balance( + address=alice_wallet.coldkeypub.ss58_address + ) + + response = await async_subtensor.staking.subnet_buyback( + wallet=alice_wallet, + netuid=alice_sn.netuid, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + amount=Balance.from_tao(10), + limit_price=Balance.from_tao(2), + period=16, + ) + assert response.success, response.message + + # stake is burned immediately after buyback + stake_after = await async_subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_sn.netuid, + ) + assert stake_after == Balance(0).set_unit(alice_sn.netuid) + + # buyback spends TAO from the subnet owner coldkey + balance_after = await async_subtensor.wallets.get_balance( + address=alice_wallet.coldkeypub.ss58_address + ) + assert balance_after < balance_before diff --git a/tests/unit_tests/extrinsics/asyncex/test_staking.py b/tests/unit_tests/extrinsics/asyncex/test_staking.py index 6d1571fcf7..2126ad3680 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_staking.py +++ b/tests/unit_tests/extrinsics/asyncex/test_staking.py @@ -1,7 +1,9 @@ import pytest from bittensor.core.extrinsics.asyncex import staking +from bittensor.core.settings import DEFAULT_MEV_PROTECTION from bittensor.core.types import ExtrinsicResponse +from bittensor.utils.balance import Balance @pytest.mark.parametrize( @@ -53,3 +55,117 @@ async def test_set_auto_stake_extrinsic( assert success is res_success assert message == res_message + + +@pytest.mark.asyncio +async def test_subnet_buyback_extrinsic(fake_wallet, mocker): + """Verify that async `subnet_buyback_extrinsic` method calls proper methods.""" + # Preps + fake_substrate = mocker.AsyncMock(**{"get_chain_head.return_value": "0xhead"}) + fake_subtensor = mocker.AsyncMock( + **{ + "get_balance.return_value": Balance.from_tao(10), + "get_existential_deposit.return_value": Balance.from_tao(1), + "get_stake.return_value": Balance.from_tao(0), + "get_block_hash.return_value": "0xblock", + "sign_and_send_extrinsic.return_value": ExtrinsicResponse(True, "Success"), + "substrate": fake_substrate, + } + ) + fake_wallet.coldkeypub.ss58_address = "coldkey" + hotkey_ss58 = "hotkey" + netuid = 1 + amount = Balance.from_tao(2) + + mocked_pallet_compose_call = mocker.AsyncMock() + mocker.patch.object( + staking.SubtensorModule, "subnet_buyback", new=mocked_pallet_compose_call + ) + fake_subtensor.sim_swap = mocker.AsyncMock( + return_value=mocker.Mock(tao_fee=Balance.from_rao(1), alpha_fee=mocker.Mock()) + ) + + # Call + result = await staking.subnet_buyback_extrinsic( + subtensor=fake_subtensor, + wallet=fake_wallet, + hotkey_ss58=hotkey_ss58, + netuid=netuid, + amount=amount, + mev_protection=DEFAULT_MEV_PROTECTION, + wait_for_inclusion=True, + wait_for_finalization=True, + wait_for_revealed_execution=True, + ) + + # Asserts + assert result.success is True + mocked_pallet_compose_call.assert_awaited_once_with( + netuid=netuid, + hotkey=hotkey_ss58, + amount=amount.rao, + limit=None, + ) + fake_subtensor.sign_and_send_extrinsic.assert_awaited_once_with( + call=mocked_pallet_compose_call.return_value, + wallet=fake_wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + nonce_key="coldkeypub", + use_nonce=True, + period=None, + raise_error=False, + ) + + +@pytest.mark.asyncio +async def test_subnet_buyback_extrinsic_with_limit(fake_wallet, mocker): + """Verify that async `subnet_buyback_extrinsic` passes limit price.""" + # Preps + fake_substrate = mocker.AsyncMock(**{"get_chain_head.return_value": "0xhead"}) + fake_subtensor = mocker.AsyncMock( + **{ + "get_balance.return_value": Balance.from_tao(10), + "get_existential_deposit.return_value": Balance.from_tao(1), + "get_stake.return_value": Balance.from_tao(0), + "get_block_hash.return_value": "0xblock", + "sign_and_send_extrinsic.return_value": ExtrinsicResponse(True, "Success"), + "substrate": fake_substrate, + } + ) + fake_wallet.coldkeypub.ss58_address = "coldkey" + hotkey_ss58 = "hotkey" + netuid = 1 + amount = Balance.from_tao(2) + limit_price = Balance.from_tao(2) + + mocked_pallet_compose_call = mocker.AsyncMock() + mocker.patch.object( + staking.SubtensorModule, "subnet_buyback", new=mocked_pallet_compose_call + ) + fake_subtensor.sim_swap = mocker.AsyncMock( + return_value=mocker.Mock(tao_fee=Balance.from_rao(1), alpha_fee=mocker.Mock()) + ) + + # Call + result = await staking.subnet_buyback_extrinsic( + subtensor=fake_subtensor, + wallet=fake_wallet, + hotkey_ss58=hotkey_ss58, + netuid=netuid, + amount=amount, + limit_price=limit_price, + mev_protection=DEFAULT_MEV_PROTECTION, + wait_for_inclusion=True, + wait_for_finalization=True, + wait_for_revealed_execution=True, + ) + + # Asserts + assert result.success is True + mocked_pallet_compose_call.assert_awaited_once_with( + netuid=netuid, + hotkey=hotkey_ss58, + amount=amount.rao, + limit=limit_price.rao, + ) diff --git a/tests/unit_tests/extrinsics/test_staking.py b/tests/unit_tests/extrinsics/test_staking.py index 77ced51faf..b559e0e381 100644 --- a/tests/unit_tests/extrinsics/test_staking.py +++ b/tests/unit_tests/extrinsics/test_staking.py @@ -2,8 +2,8 @@ from bittensor.core.extrinsics import staking from bittensor.core.settings import DEFAULT_MEV_PROTECTION -from bittensor.utils.balance import Balance from bittensor.core.types import ExtrinsicResponse +from bittensor.utils.balance import Balance def test_add_stake_extrinsic(mocker): @@ -65,6 +65,114 @@ def test_add_stake_extrinsic(mocker): ) +def test_subnet_buyback_extrinsic(mocker): + """Verify that sync `subnet_buyback_extrinsic` method calls proper methods.""" + # Preps + fake_subtensor = mocker.Mock( + **{ + "get_balance.return_value": Balance.from_tao(10), + "get_existential_deposit.return_value": Balance.from_tao(1), + "get_current_block.return_value": 123, + "get_stake.return_value": Balance.from_tao(0), + "sign_and_send_extrinsic.return_value": ExtrinsicResponse(True, "Success"), + } + ) + fake_wallet = mocker.Mock( + **{ + "coldkeypub.ss58_address": "coldkey", + } + ) + hotkey_ss58 = "hotkey" + netuid = 1 + amount = Balance.from_tao(2) + + # Call + result = staking.subnet_buyback_extrinsic( + subtensor=fake_subtensor, + wallet=fake_wallet, + hotkey_ss58=hotkey_ss58, + netuid=netuid, + amount=amount, + mev_protection=DEFAULT_MEV_PROTECTION, + wait_for_inclusion=True, + wait_for_finalization=True, + wait_for_revealed_execution=True, + ) + + # Asserts + assert result.success is True + fake_subtensor.compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="subnet_buyback", + call_params={ + "hotkey": hotkey_ss58, + "netuid": netuid, + "amount": amount.rao, + "limit": None, + }, + ) + fake_subtensor.sign_and_send_extrinsic.assert_called_once_with( + call=fake_subtensor.compose_call.return_value, + wallet=fake_wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + nonce_key="coldkeypub", + use_nonce=True, + period=None, + raise_error=False, + ) + + +def test_subnet_buyback_extrinsic_with_limit(mocker): + """Verify that sync `subnet_buyback_extrinsic` passes limit price.""" + # Preps + fake_subtensor = mocker.Mock( + **{ + "get_balance.return_value": Balance.from_tao(10), + "get_existential_deposit.return_value": Balance.from_tao(1), + "get_current_block.return_value": 123, + "get_stake.return_value": Balance.from_tao(0), + "sign_and_send_extrinsic.return_value": ExtrinsicResponse(True, "Success"), + } + ) + fake_wallet = mocker.Mock( + **{ + "coldkeypub.ss58_address": "coldkey", + } + ) + hotkey_ss58 = "hotkey" + netuid = 1 + amount = Balance.from_tao(2) + limit_price = Balance.from_tao(2) + + # Call + result = staking.subnet_buyback_extrinsic( + subtensor=fake_subtensor, + wallet=fake_wallet, + hotkey_ss58=hotkey_ss58, + netuid=netuid, + amount=amount, + limit_price=limit_price, + mev_protection=DEFAULT_MEV_PROTECTION, + wait_for_inclusion=True, + wait_for_finalization=True, + wait_for_revealed_execution=True, + ) + + # Asserts + assert result.success is True + fake_subtensor.compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="subnet_buyback", + call_params={ + "hotkey": hotkey_ss58, + "netuid": netuid, + "amount": amount.rao, + "limit": limit_price.rao, + }, + ) + + def test_add_stake_multiple_extrinsic(subtensor, mocker, fake_wallet): """Verify that sync `add_stake_multiple_extrinsic` method calls proper async method.""" # Preps diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 8a919b90ce..ae657c55e3 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -2920,6 +2920,71 @@ async def test_start_call(subtensor, mocker): assert result == mocked_extrinsic.return_value +@pytest.mark.asyncio +async def test_subnet_buyback(subtensor, mocker): + """Test subnet_buyback extrinsic calls properly.""" + # Preps + wallet_name = mocker.Mock(spec=Wallet) + netuid = 123 + hotkey_ss58 = "hotkey" + amount = Balance.from_tao(1.0) + mocked_extrinsic = mocker.patch.object(async_subtensor, "subnet_buyback_extrinsic") + + # Call + result = await subtensor.subnet_buyback(wallet_name, netuid, hotkey_ss58, amount) + + # Asserts + mocked_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + wallet=wallet_name, + netuid=netuid, + hotkey_ss58=hotkey_ss58, + amount=amount, + limit_price=None, + mev_protection=DEFAULT_MEV_PROTECTION, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + wait_for_revealed_execution=True, + ) + assert result == mocked_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_subnet_buyback_with_limit_price(subtensor, mocker): + """Test subnet_buyback extrinsic passes limit price.""" + # Preps + wallet_name = mocker.Mock(spec=Wallet) + netuid = 123 + hotkey_ss58 = "hotkey" + amount = Balance.from_tao(1.0) + limit_price = Balance.from_tao(2.0) + mocked_extrinsic = mocker.patch.object(async_subtensor, "subnet_buyback_extrinsic") + + # Call + result = await subtensor.subnet_buyback( + wallet_name, netuid, hotkey_ss58, amount, limit_price=limit_price + ) + + # Asserts + mocked_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + wallet=wallet_name, + netuid=netuid, + hotkey_ss58=hotkey_ss58, + amount=amount, + limit_price=limit_price, + mev_protection=DEFAULT_MEV_PROTECTION, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + wait_for_revealed_execution=True, + ) + assert result == mocked_extrinsic.return_value + + @pytest.mark.asyncio async def test_get_metagraph_info_all_fields(subtensor, mocker): """Test get_metagraph_info with all fields (default behavior).""" diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 51e5df282f..2b4e4eea62 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -3182,6 +3182,76 @@ def test_start_call(subtensor, mocker): assert result == mocked_extrinsic.return_value +def test_subnet_buyback(subtensor, fake_wallet, mocker): + """Test subnet_buyback extrinsic calls properly.""" + # Preps + netuid = 123 + hotkey_ss58 = "hotkey" + amount = Balance.from_tao(1.0) + mocked_extrinsic = mocker.patch.object(subtensor_module, "subnet_buyback_extrinsic") + + # Call + result = subtensor.subnet_buyback( + wallet=fake_wallet, + netuid=netuid, + hotkey_ss58=hotkey_ss58, + amount=amount, + ) + + # Asserts + mocked_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + hotkey_ss58=hotkey_ss58, + amount=amount, + limit_price=None, + mev_protection=DEFAULT_MEV_PROTECTION, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + wait_for_revealed_execution=True, + ) + assert result == mocked_extrinsic.return_value + + +def test_subnet_buyback_with_limit_price(subtensor, fake_wallet, mocker): + """Test subnet_buyback extrinsic passes limit price.""" + # Preps + netuid = 123 + hotkey_ss58 = "hotkey" + amount = Balance.from_tao(1.0) + limit_price = Balance.from_tao(2.0) + mocked_extrinsic = mocker.patch.object(subtensor_module, "subnet_buyback_extrinsic") + + # Call + result = subtensor.subnet_buyback( + wallet=fake_wallet, + netuid=netuid, + hotkey_ss58=hotkey_ss58, + amount=amount, + limit_price=limit_price, + ) + + # Asserts + mocked_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + hotkey_ss58=hotkey_ss58, + amount=amount, + limit_price=limit_price, + mev_protection=DEFAULT_MEV_PROTECTION, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + wait_for_revealed_execution=True, + ) + assert result == mocked_extrinsic.return_value + + def test_get_metagraph_info_all_fields(subtensor, mocker): """Test get_metagraph_info with all fields (default behavior).""" # Preps