diff --git a/MIGRATION.md b/MIGRATION.md index d248ac331b..44e7e72e2d 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -12,7 +12,7 @@ ## Subtensor 1. ✅ In the synchronous Subtensor class, the `get_owned_hotkeys` method includes a `reuse_block` parameter that is inconsistent with other methods. Either remove this parameter from `get_owned_hotkeys`, or add it to all other methods that directly call self.substrate.* to maintain a consistent interface. -2. ✅ In all methods where we `get_stake_operations_fee` is called, remove unused arguments. Consider combining all methods using `get_stake_operations_fee` into one common one. +2. ✅ In all methods where we `sim_swap` is called, remove unused arguments. Consider combining all methods using `sim_swap` into one common one. 3. ✅ Delete deprecated `get_current_weight_commit_info` and `get_current_weight_commit_info_v2`. ~~Rename `get_timelocked_weight_commits` to `get_current_weight_commit_info`.~~ 4. ✅ Remove references like `get_stake_info_for_coldkey = get_stake_for_coldkey`. 5. ✅ Reconsider some methods naming across the entire subtensor module. @@ -21,7 +21,7 @@ 8. ✅ Should the next functions move to `subtensor` as methods? They have exactly the same behavior as subtensor methods. - `get_metadata` - `get_last_bonds_reset` -9. Apply `SimSwap` logic to calculate any stake operation fees (this is not an extrinsic fee) +9. ✅ Apply `SimSwap` logic to calculate any stake operation fees (this is not an extrinsic fee) ## Metagraph 1. ✅ Remove verbose archival node warnings for blocks older than 300. Some users complained about many messages for them. @@ -86,9 +86,12 @@ - header (dict) - extrinsics (list) - block_explorer (link to tao.app) - + This implementation has been repeatedly requested by the community in the past. -5. Implement `Crowdloan` logic. Issue: https://github.com/opentensor/bittensor/issues/3017 +5. ✅ Added `bittensor.core.extrinsics.params` subpackage. This package will contain all extrinsic parameters. Due to + the duplication of extrinsics (async and sync implementations), it's easy to miss the sequence of changes. This also + makes it easier to obtain the parameter list. +6. Implement `Crowdloan` logic. Issue: https://github.com/opentensor/bittensor/issues/3017 ## Testing 1. ✅ When running tests via Docker, ensure no lingering processes occupy required ports before launch. @@ -147,6 +150,7 @@ wait_for_inclusion: bool = True, wait_for_finalization: bool = True, ``` - [x] `.set_children_extrinsic` and `.root_set_pending_childkey_cooldown_extrinsic`. `subtensor.set_children` and `subtensor.root_set_pending_childkey_cooldown` methods. + - parameters re-ordered. All extrinsics and related call has the same oder schema: `subtensor, netuid, hotkey_ss58` ... Another order confuses ppl. - [x] `.add_liquidity_extrinsic` and `subtensor.add_liquidity` - [x] `.modify_liquidity_extrinsic` and `subtensor.modify_liquidity` - [x] `.remove_liquidity_extrinsic` and `subtensor.remove_liquidity` @@ -159,6 +163,7 @@ wait_for_finalization: bool = True, - Changes in `move_stake_extrinsic` and `subtensor.move_stake`: - parameter `origin_hotkey` renamed to `origin_hotkey_ss58` - parameter `destination_hotkey` renamed to `destination_hotkey_ss58` + - parameters re-ordered. All extrinsics and related call has the same oder schema: `subtensor, netuid, hotkey_ss58` ... Another order confuses ppl. - [x] `.burned_register_extrinsic` and `subtensor.burned_register` - [x] `.register_subnet_extrinsic` and `subtensor.register_subnet` - [x] `.register_extrinsic` and `subtensor.register` @@ -193,6 +198,7 @@ wait_for_finalization: bool = True, - parameter `safe_staking: bool` renamed to `safe_unstaking: bool` - parameter `unstake_all: bool` removed (use `unstake_all_extrinsic` for unstake all stake) - [x] `.unstake_all_extrinsic` and `subtensor.unstake_all` + - parameters re-ordered. All extrinsics and related call has the same oder schema: `subtensor, netuid, hotkey_ss58` ... Another order confuses ppl. - [x] `.unstake_multiple_extrinsic` and `subtensor.unstake_multiple` - Changes in `.unstake_multiple_extrinsic` and `subtensor.unstake_multiple`: - parameter `amounts` is now required (no Optional anymore) @@ -256,7 +262,12 @@ Removing deprecated extrinsics and replacing them with consistent ones: - method `get_traansfer_fee` has renamed parameter `value` to `amount` - `bittensor.core.extrinsic.serving.get_metadata` functions moved to `subtensor.get_commitment_metadata` method - `bittensor.core.extrinsic.serving.get_last_bonds_reset` function moved to `subtensor.get_last_bonds_reset` method - +- added method `subtensor.get_extrinsic_fee` +- added method `subtensor.compose_call` +- added method `subtensor.sim_swap` +- added method `subtensor.validate_extrinsic_params` +- methods `get_stake_add_fee`, `get_stake_movement_fee`, `get_unstake_fee` have updated parameters order. + Added sub-package `bittensor.core.addons` to host optional extensions and experimental logic enhancing the core functionality. - `bittensor.core.subtensor_api` moved to `bittensor.core.addons.subtensor_api` - `bittensor.core.timelock` moved to `bittensor.core.addons.timelock` diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 5ffa1004d5..beb90c93c7 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -21,6 +21,7 @@ NeuronInfo, ProposalVoteData, SelectiveMetagraphIndex, + SimSwapResult, StakeInfo, SubnetHyperparameters, SubnetIdentity, @@ -77,13 +78,13 @@ unstake_extrinsic, unstake_multiple_extrinsic, ) -from bittensor.core.extrinsics.asyncex.utils import get_extrinsic_fee from bittensor.core.extrinsics.asyncex.weights import ( commit_timelocked_weights_extrinsic, commit_weights_extrinsic, reveal_weights_extrinsic, set_weights_extrinsic, ) +from bittensor.core.extrinsics.params.transfer import get_transfer_fn_params from bittensor.core.metagraph import AsyncMetagraph from bittensor.core.settings import ( version_as_int, @@ -104,7 +105,6 @@ decode_hex_identity_dict, format_error_message, get_caller_name, - get_transfer_fn_params, get_mechid_storage_index, is_valid_ss58_address, u16_normalized_float, @@ -123,13 +123,10 @@ price_to_tick, LiquidityPosition, ) -from bittensor.utils.weight_utils import ( - U16_MAX, -) if TYPE_CHECKING: from async_substrate_interface.types import ScaleObj - from bittensor_wallet import Wallet + from bittensor_wallet import Keypair, Wallet from bittensor.core.axon import Axon from async_substrate_interface import AsyncQueryMapResult @@ -354,6 +351,11 @@ def _get_substrate( ws_shutdown_timer=ws_shutdown_timer, ) + @property + async def block(self): + """Provides an asynchronous property to retrieve the current block.""" + return await self.get_current_block() + async def determine_block_hash( self, block: Optional[int] = None, @@ -515,10 +517,80 @@ async def get_hyperparameter( return getattr(result, "value", result) - @property - async def block(self): - """Provides an asynchronous property to retrieve the current block.""" - return await self.get_current_block() + async def sim_swap( + self, + origin_netuid: int, + destination_netuid: int, + amount: "Balance", + block_hash: Optional[str] = None, + ) -> SimSwapResult: + """ + Hits the SimSwap Runtime API to calculate the fee and result for a given transaction. The SimSwapResult contains + the staking fees and expected returned amounts of a given transaction. This does not include the transaction + (extrinsic) fee. + + Args: + origin_netuid: Netuid of the source subnet (0 if add stake). + destination_netuid: Netuid of the destination subnet. + amount: Amount to stake operation. + block_hash: The hash of the blockchain block number for the query. + + Returns: + SimSwapResult object representing the result. + """ + check_balance_amount(amount) + block_hash = block_hash or await self.substrate.get_chain_head() + if origin_netuid > 0 and destination_netuid > 0: + # for cross-subnet moves where neither origin nor destination is root + intermediate_result_, sn_price = await asyncio.gather( + self.query_runtime_api( + runtime_api="SwapRuntimeApi", + method="sim_swap_alpha_for_tao", + params={"netuid": origin_netuid, "alpha": amount.rao}, + block_hash=block_hash, + ), + self.get_subnet_price(origin_netuid, block_hash=block_hash), + ) + intermediate_result = SimSwapResult.from_dict( + intermediate_result_, origin_netuid + ) + result = SimSwapResult.from_dict( + await self.query_runtime_api( + runtime_api="SwapRuntimeApi", + method="sim_swap_tao_for_alpha", + params={ + "netuid": destination_netuid, + "tao": intermediate_result.tao_amount.rao, + }, + block_hash=block_hash, + ), + origin_netuid, + ) + secondary_fee = (result.tao_fee / sn_price.tao).set_unit(origin_netuid) + result.alpha_fee = result.alpha_fee + secondary_fee + return result + elif origin_netuid > 0: + # dynamic to tao + return SimSwapResult.from_dict( + await self.query_runtime_api( + runtime_api="SwapRuntimeApi", + method="sim_swap_alpha_for_tao", + params={"netuid": origin_netuid, "alpha": amount.rao}, + block_hash=block_hash, + ), + origin_netuid, + ) + else: + # tao to dynamic or unstaked to staked tao (SN0) + return SimSwapResult.from_dict( + await self.query_runtime_api( + runtime_api="SwapRuntimeApi", + method="sim_swap_tao_for_alpha", + params={"netuid": destination_netuid, "tao": amount.rao}, + block_hash=block_hash, + ), + destination_netuid, + ) # Subtensor queries =========================================================================================== @@ -2791,12 +2863,13 @@ async def get_stake( return Balance.from_rao(int(stake)).set_unit(netuid=netuid) - # TODO: update related with fee calculation async def get_stake_add_fee( self, amount: Balance, netuid: int, block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, ) -> Balance: """ Calculates the fee for adding new stake to a hotkey. @@ -2804,38 +2877,55 @@ async def get_stake_add_fee( Parameters: amount: Amount of stake to add in TAO netuid: Netuid of subnet - block: Block number at which to perform the calculation + block: The block number for which the children are to be retrieved. + block_hash: The hash of the block to retrieve the subnet unique identifiers from. + reuse_block: Whether to reuse the last-used block hash. Returns: - The calculated stake fee as a Balance object + The calculated stake fee as a Balance object in TAO. """ check_balance_amount(amount) - return await self.get_stake_operations_fee( - amount=amount, netuid=netuid, block=block + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + sim_swap_result = await self.sim_swap( + origin_netuid=0, + destination_netuid=netuid, + amount=amount, + block_hash=block_hash, ) + return sim_swap_result.tao_fee - # TODO: update related with fee calculation async def get_stake_movement_fee( self, - amount: Balance, origin_netuid: int, + destination_netuid: int, + amount: Balance, block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, ) -> Balance: """ Calculates the fee for moving stake between hotkeys/subnets/coldkeys. Parameters: - amount: Amount of stake to move in TAO - origin_netuid: Netuid of source subnet - block: Block number at which to perform the calculation + origin_netuid: Netuid of source subnet. + destination_netuid: Netuid of the destination subnet. + amount: Amount of stake to move. + block: The block number for which the children are to be retrieved. + block_hash: The hash of the block to retrieve the subnet unique identifiers from. + reuse_block: Whether to reuse the last-used block hash. Returns: The calculated stake fee as a Balance object """ check_balance_amount(amount) - return await self.get_stake_operations_fee( - amount=amount, netuid=origin_netuid, block=block + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + sim_swap_result = await self.sim_swap( + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + amount=amount, + block_hash=block_hash, ) + return sim_swap_result.tao_fee async def get_stake_for_coldkey_and_hotkey( self, @@ -2950,38 +3040,6 @@ async def get_stake_for_hotkey( get_hotkey_stake = get_stake_for_hotkey - async def get_stake_operations_fee( - self, - amount: Balance, - netuid: int, - block: Optional[int] = None, - block_hash: Optional[str] = None, - reuse_block: bool = False, - ): - """Returns fee for any stake operation in specified subnet. - - Parameters: - amount: Amount of stake to add in Alpha/TAO. - netuid: Netuid of subnet. - block: The block number to query. Do not specify if using block_hash or reuse_block. - block_hash: The block hash at which to check the parameter. Do not set if using block or reuse_block. - reuse_block: Whether to reuse the last-used block hash. Do not set if using block_hash or block. - - Returns: - The calculated stake fee as a Balance object. - """ - check_balance_amount(amount) - block_hash = await self.determine_block_hash( - block=block, block_hash=block_hash, reuse_block=reuse_block - ) - result = await self.substrate.query( - module="Swap", - storage_function="FeeRate", - params=[netuid], - block_hash=block_hash, - ) - return amount * (result.value / U16_MAX) - async def get_stake_weight( self, netuid: int, @@ -3355,7 +3413,6 @@ async def get_total_subnets( ) return getattr(result, "value", None) - # TODO: update related with fee calculation async def get_transfer_fee( self, wallet: "Wallet", dest: str, amount: Balance, keep_alive: bool = True ) -> Balance: @@ -3384,7 +3441,7 @@ async def get_transfer_fee( call_params: dict[str, Union[int, str, bool]] call_function, call_params = get_transfer_fn_params(amount, dest, keep_alive) - call = await self.substrate.compose_call( + call = await self.compose_call( call_module="Balances", call_function=call_function, call_params=call_params, @@ -3400,28 +3457,36 @@ async def get_transfer_fee( return Balance.from_rao(payment_info["partial_fee"]) - # TODO: update related with fee calculation async def get_unstake_fee( self, - amount: Balance, netuid: int, + amount: Balance, block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, ) -> Balance: """ Calculates the fee for unstaking from a hotkey. Parameters: - amount: Amount of stake to unstake in TAO - netuid: Netuid of subnet - block: Block number at which to perform the calculation + netuid: The unique identifier of the subnet. + amount: Amount of stake to unstake in TAO. + block: The blockchain block number for the query. + block_hash: The blockchain block_hash representation of the block id. + reuse_block: Whether to reuse the last-used blockchain block hash. Returns: - The calculated stake fee as a Balance object + The calculated stake fee as a Balance object in Alpha. """ check_balance_amount(amount) - return await self.get_stake_operations_fee( - amount=amount, netuid=netuid, block=block + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + sim_swap_result = await self.sim_swap( + origin_netuid=netuid, + destination_netuid=0, + amount=amount, + block_hash=block_hash, ) + return sim_swap_result.alpha_fee.set_unit(netuid=netuid) async def get_vote_data( self, @@ -4353,7 +4418,119 @@ async def weights_rate_limit( ) return None if call is None else int(call) - # Extrinsics helper ================================================================================================ + # Extrinsics helpers =============================================================================================== + async def validate_extrinsic_params( + self, + call_module: str, + call_function: str, + call_params: dict[str, Any], + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ): + """ + Validate and filter extrinsic parameters against on-chain metadata. + + This method checks that the provided parameters match the expected signature of the given extrinsic (module and + function) as defined in the Substrate metadata. It raises explicit errors for missing or invalid parameters and + silently ignores any extra keys not present in the function definition. + + Args: + call_module: The pallet name, e.g. "SubtensorModule" or "AdminUtils". + call_function: The extrinsic function name, e.g. "set_weights" or "sudo_set_tempo". + call_params: A dictionary of parameters to validate. + block: The blockchain block number for the query. + block_hash: The blockchain block_hash representation of the block id. + reuse_block: Whether to reuse the last-used blockchain block hash. + + Returns: + A filtered dictionary containing only the parameters that are valid for the specified extrinsic. + + Raises: + ValueError: If the given module or function is not found in the chain metadata. + KeyError: If one or more required parameters are missing. + + Notes: + This method does not compose or submit the extrinsic. It only ensures that `call_params` conforms to the + expected schema derived from on-chain metadata. + """ + block_hash = await self.determine_block_hash( + block=block, block_hash=block_hash, reuse_block=reuse_block + ) + + func_meta = await self.substrate.get_metadata_call_function( + module_name=call_module, + call_function_name=call_function, + block_hash=block_hash, + ) + + if not func_meta: + raise ValueError( + f"Call {call_module}.{call_function} not found in chain metadata." + ) + + # Expected params from metadata + expected_params = func_meta.get_param_info() + provided_params = {} + + # Validate and filter parameters + for param_name in expected_params.keys(): + if param_name not in call_params: + raise KeyError(f"Missing required parameter: '{param_name}'") + provided_params[param_name] = call_params[param_name] + + # Warn about extra params not defined in metadata + extra_params = set(call_params.keys()) - set(expected_params.keys()) + if extra_params: + logging.debug( + f"Ignoring extra parameters for {call_module}.{call_function}: {extra_params}." + ) + return provided_params + + async def compose_call( + self, + call_module: str, + call_function: str, + call_params: dict[str, Any], + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> "GenericCall": + """ + Dynamically compose a GenericCall using on-chain Substrate metadata after validating the provided parameters. + + Args: + call_module: Pallet name (e.g. "SubtensorModule", "AdminUtils"). + call_function: Function name (e.g. "set_weights", "sudo_set_tempo"). + call_params: Dictionary of parameters for the call. + block: The blockchain block number for the query. + block_hash: The blockchain block_hash representation of the block id. + reuse_block: Whether to reuse the last-used blockchain block hash. + + Returns: + GenericCall: Composed call object ready for extrinsic submission. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + + call_params = await self.validate_extrinsic_params( + call_module=call_module, + call_function=call_function, + call_params=call_params, + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + + logging.debug( + f"Composing GenericCall -> {call_module}.{call_function} " + f"with params: {call_params}." + ) + return await self.substrate.compose_call( + call_module=call_module, + call_function=call_function, + call_params=call_params, + block_hash=block_hash, + ) async def sign_and_send_extrinsic( self, @@ -4415,8 +4592,8 @@ async def sign_and_send_extrinsic( if period is not None: extrinsic_data["era"] = {"period": period} - extrinsic_response.extrinsic_fee = await get_extrinsic_fee( - subtensor=self, call=call, keypair=signing_keypair + extrinsic_response.extrinsic_fee = await self.get_extrinsic_fee( + call=call, keypair=signing_keypair ) extrinsic_response.extrinsic = await self.substrate.create_signed_extrinsic( **extrinsic_data @@ -4459,6 +4636,27 @@ async def sign_and_send_extrinsic( extrinsic_response.error = error return extrinsic_response + async def get_extrinsic_fee( + self, + call: "GenericCall", + keypair: "Keypair", + ): + """ + Get extrinsic fee for a given extrinsic call and keypair for a given SN's netuid. + + Parameters: + call: The extrinsic GenericCall. + keypair: The keypair associated with the extrinsic. + + Returns: + Balance object representing the extrinsic fee in RAO. + + Note: + To create the GenericCall object use `compose_call` method with proper parameters. + """ + payment_info = await self.substrate.get_payment_info(call=call, keypair=keypair) + return Balance.from_rao(amount=payment_info["partial_fee"]) + # Extrinsics ======================================================================================================= async def add_stake( @@ -4819,10 +5017,10 @@ async def modify_liquidity( async def move_stake( self, wallet: "Wallet", - origin_hotkey_ss58: str, origin_netuid: int, - destination_hotkey_ss58: str, + origin_hotkey_ss58: str, destination_netuid: int, + destination_hotkey_ss58: str, amount: Optional[Balance] = None, move_all_stake: bool = False, period: Optional[int] = None, @@ -4835,10 +5033,10 @@ async def move_stake( Parameters: wallet: The wallet to move stake from. - origin_hotkey_ss58: The SS58 address of the source hotkey. origin_netuid: The netuid of the source subnet. - destination_hotkey_ss58: The SS58 address of the destination hotkey. + origin_hotkey_ss58: The SS58 address of the source hotkey. destination_netuid: The netuid of the destination subnet. + destination_hotkey_ss58: The SS58 address of the destination hotkey. amount: Amount of stake to move. move_all_stake: If true, moves all stake from the source hotkey to the destination hotkey. period: The number of blocks during which the transaction will remain valid after it's submitted. If the @@ -4855,10 +5053,10 @@ async def move_stake( return await move_stake_extrinsic( subtensor=self, wallet=wallet, - origin_hotkey_ss58=origin_hotkey_ss58, origin_netuid=origin_netuid, - destination_hotkey_ss58=destination_hotkey_ss58, + origin_hotkey_ss58=origin_hotkey_ss58, destination_netuid=destination_netuid, + destination_hotkey_ss58=destination_hotkey_ss58, amount=amount, move_all_stake=move_all_stake, period=period, @@ -5954,8 +6152,8 @@ async def unstake( async def unstake_all( self, wallet: "Wallet", - hotkey_ss58: str, netuid: int, + hotkey_ss58: str, rate_tolerance: Optional[float] = 0.005, period: Optional[int] = None, raise_error: bool = False, @@ -5966,8 +6164,8 @@ async def unstake_all( Parameters: wallet: The wallet of the stake owner. - hotkey_ss58: The SS58 address of the hotkey to unstake from. netuid: The unique identifier of the subnet. + hotkey_ss58: The SS58 address of the hotkey to unstake from. rate_tolerance: The maximum allowed price change ratio when unstaking. For example, 0.005 = 0.5% maximum price decrease. If not passed (None), then unstaking goes without price limit. period: The number of blocks during which the transaction will remain valid after it's submitted. If @@ -6022,8 +6220,8 @@ async def unstake_all( return await unstake_all_extrinsic( subtensor=self, wallet=wallet, - hotkey_ss58=hotkey_ss58, netuid=netuid, + hotkey_ss58=hotkey_ss58, rate_tolerance=rate_tolerance, period=period, raise_error=raise_error, diff --git a/bittensor/core/chain_data/__init__.py b/bittensor/core/chain_data/__init__.py index e2aee44a4a..6a423501f7 100644 --- a/bittensor/core/chain_data/__init__.py +++ b/bittensor/core/chain_data/__init__.py @@ -24,6 +24,7 @@ from .proposal_vote_data import ProposalVoteData from .scheduled_coldkey_swap_info import ScheduledColdkeySwapInfo from .stake_info import StakeInfo +from .sim_swap import SimSwapResult from .subnet_hyperparameters import SubnetHyperparameters from .subnet_identity import SubnetIdentity from .subnet_info import SubnetInfo @@ -52,6 +53,7 @@ "ProposalVoteData", "ScheduledColdkeySwapInfo", "SelectiveMetagraphIndex", + "SimSwapResult", "StakeInfo", "SubnetHyperparameters", "SubnetIdentity", diff --git a/bittensor/core/chain_data/sim_swap.py b/bittensor/core/chain_data/sim_swap.py new file mode 100644 index 0000000000..42afae7fd4 --- /dev/null +++ b/bittensor/core/chain_data/sim_swap.py @@ -0,0 +1,48 @@ +from dataclasses import dataclass + +from bittensor.utils.balance import Balance + + +@dataclass +class SimSwapResult: + """ + Represents the result of a simulated swap operation. + + This class is used to encapsulate the amounts and fees for the simulated swap process, including both tao and alpha token values. + It provides a convenient way to manage and interpret the swap results. + + Attributes: + tao_amount: The amount of tao tokens obtained as the result of the swap. + alpha_amount: The amount of alpha tokens obtained as the result of the swap. + tao_fee: The fee associated with the tao token portion of the swap. + alpha_fee: The fee associated with the alpha token portion of the swap. + """ + + tao_amount: Balance + alpha_amount: Balance + tao_fee: Balance + alpha_fee: Balance + + @classmethod + def from_dict(cls, data: dict, netuid: int) -> "SimSwapResult": + """ + Converts a dictionary to a SimSwapResult instance. + + This method acts as a factory to create a SimSwapResult object using the data + from a dictionary. It parses the specified dictionary, converts values into + Balance objects, and sets associated units based on parameters and context. + + Args: + data: A dictionary containing the swap result data. It must include the keys "tao_amount", "alpha_amount", + "tao_fee", and "alpha_fee" with their respective values. + netuid: A network-specific unit identifier used to set the unit for alpha-related amounts. + + Returns: + SimSwapResult: An instance of SimSwapResult initialized with the parsed and converted data. + """ + return cls( + tao_amount=Balance.from_rao(data["tao_amount"]).set_unit(0), + alpha_amount=Balance.from_rao(data["alpha_amount"]).set_unit(netuid), + tao_fee=Balance.from_rao(data["tao_fee"]).set_unit(0), + alpha_fee=Balance.from_rao(data["alpha_fee"]).set_unit(netuid), + ) diff --git a/bittensor/core/extrinsics/asyncex/children.py b/bittensor/core/extrinsics/asyncex/children.py index f758d3e4cc..0bff616565 100644 --- a/bittensor/core/extrinsics/asyncex/children.py +++ b/bittensor/core/extrinsics/asyncex/children.py @@ -1,8 +1,8 @@ from typing import TYPE_CHECKING, Optional -from bittensor.core.types import ExtrinsicResponse from bittensor.core.extrinsics.asyncex.utils import sudo_call_extrinsic -from bittensor.utils import float_to_u64 +from bittensor.core.extrinsics.params import ChildrenParams +from bittensor.core.types import ExtrinsicResponse if TYPE_CHECKING: from bittensor_wallet import Wallet @@ -58,20 +58,10 @@ async def set_children_extrinsic( ).success: return unlocked - call = await subtensor.substrate.compose_call( + call = await subtensor.compose_call( call_module="SubtensorModule", call_function="set_children", - call_params={ - "children": [ - ( - float_to_u64(proportion), - child_hotkey, - ) - for proportion, child_hotkey in children - ], - "hotkey": hotkey_ss58, - "netuid": netuid, - }, + call_params=ChildrenParams.set_children(hotkey_ss58, netuid, children), ) response = await subtensor.sign_and_send_extrinsic( @@ -119,7 +109,7 @@ async def root_set_pending_childkey_cooldown_extrinsic( wallet=wallet, call_module="SubtensorModule", call_function="set_pending_childkey_cooldown", - call_params={"cooldown": cooldown}, + call_params=ChildrenParams.set_pending_childkey_cooldown(cooldown), period=period, raise_error=raise_error, wait_for_inclusion=wait_for_inclusion, diff --git a/bittensor/core/extrinsics/asyncex/liquidity.py b/bittensor/core/extrinsics/asyncex/liquidity.py index eb42ce2679..3f6cba7a7e 100644 --- a/bittensor/core/extrinsics/asyncex/liquidity.py +++ b/bittensor/core/extrinsics/asyncex/liquidity.py @@ -1,8 +1,8 @@ from typing import Optional, TYPE_CHECKING +from bittensor.core.extrinsics.params import LiquidityParams from bittensor.core.types import ExtrinsicResponse from bittensor.utils.balance import Balance -from bittensor.utils.liquidity import price_to_tick if TYPE_CHECKING: from bittensor_wallet import Wallet @@ -47,24 +47,24 @@ async def add_liquidity_extrinsic( `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. """ try: + unlock_type = "coldkey" if hotkey_ss58 else "both" if not ( - unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + unlocked := ExtrinsicResponse.unlock_wallet( + wallet, raise_error, unlock_type=unlock_type + ) ).success: return unlocked - tick_low = price_to_tick(price_low.tao) - tick_high = price_to_tick(price_high.tao) - - call = await subtensor.substrate.compose_call( + call = await subtensor.compose_call( call_module="Swap", call_function="add_liquidity", - call_params={ - "hotkey": hotkey_ss58 or wallet.hotkey.ss58_address, - "netuid": netuid, - "tick_low": tick_low, - "tick_high": tick_high, - "liquidity": liquidity.rao, - }, + call_params=LiquidityParams.add_liquidity( + netuid=netuid, + hotkey_ss58=hotkey_ss58 or wallet.hotkey.ss58_address, + liquidity=liquidity, + price_low=price_low, + price_high=price_high, + ), ) return await subtensor.sign_and_send_extrinsic( @@ -114,20 +114,23 @@ async def modify_liquidity_extrinsic( Call `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. """ try: + unlock_type = "coldkey" if hotkey_ss58 else "both" if not ( - unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + unlocked := ExtrinsicResponse.unlock_wallet( + wallet, raise_error, unlock_type + ) ).success: return unlocked - call = await subtensor.substrate.compose_call( + call = await subtensor.compose_call( call_module="Swap", call_function="modify_position", - call_params={ - "hotkey": hotkey_ss58 or wallet.hotkey.ss58_address, - "netuid": netuid, - "position_id": position_id, - "liquidity_delta": liquidity_delta.rao, - }, + call_params=LiquidityParams.modify_position( + netuid=netuid, + hotkey_ss58=hotkey_ss58 or wallet.hotkey.ss58_address, + position_id=position_id, + liquidity_delta=liquidity_delta, + ), ) return await subtensor.sign_and_send_extrinsic( @@ -175,19 +178,22 @@ async def remove_liquidity_extrinsic( `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. """ try: + unlock_type = "coldkey" if hotkey_ss58 else "both" if not ( - unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + unlocked := ExtrinsicResponse.unlock_wallet( + wallet, raise_error, unlock_type + ) ).success: return unlocked - call = await subtensor.substrate.compose_call( + call = await subtensor.compose_call( call_module="Swap", call_function="remove_liquidity", - call_params={ - "hotkey": hotkey_ss58 or wallet.hotkey.ss58_address, - "netuid": netuid, - "position_id": position_id, - }, + call_params=LiquidityParams.remove_liquidity( + netuid=netuid, + hotkey_ss58=hotkey_ss58 or wallet.hotkey.ss58_address, + position_id=position_id, + ), ) return await subtensor.sign_and_send_extrinsic( @@ -235,10 +241,13 @@ async def toggle_user_liquidity_extrinsic( ).success: return unlocked - call = await subtensor.substrate.compose_call( + call = await subtensor.compose_call( call_module="Swap", call_function="toggle_user_liquidity", - call_params={"netuid": netuid, "enable": enable}, + call_params=LiquidityParams.toggle_user_liquidity( + netuid=netuid, + enable=enable, + ), ) return await subtensor.sign_and_send_extrinsic( diff --git a/bittensor/core/extrinsics/asyncex/move_stake.py b/bittensor/core/extrinsics/asyncex/move_stake.py index 192b94e7e0..cea298433b 100644 --- a/bittensor/core/extrinsics/asyncex/move_stake.py +++ b/bittensor/core/extrinsics/asyncex/move_stake.py @@ -1,6 +1,7 @@ import asyncio from typing import Optional, TYPE_CHECKING +from bittensor.core.extrinsics.params import MoveStakeParams from bittensor.core.types import ExtrinsicResponse from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging @@ -38,30 +39,32 @@ async def _get_stake_in_origin_and_dest( return stake_in_origin, stake_in_destination -async def transfer_stake_extrinsic( +async def move_stake_extrinsic( subtensor: "AsyncSubtensor", wallet: "Wallet", - destination_coldkey_ss58: str, - hotkey_ss58: str, origin_netuid: int, + origin_hotkey_ss58: str, destination_netuid: int, - amount: Balance, + destination_hotkey_ss58: str, + amount: Optional[Balance] = None, + move_all_stake: bool = False, period: Optional[int] = None, raise_error: bool = False, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, ) -> ExtrinsicResponse: """ - Transfers stake from one coldkey to another in the Bittensor network. + Moves stake from one hotkey to another within subnets in the Bittensor network. Parameters: - subtensor: The subtensor instance to interact with the blockchain. - wallet: The wallet containing the coldkey to authorize the transfer. - destination_coldkey_ss58: SS58 address of the destination coldkey. - hotkey_ss58: SS58 address of the hotkey associated with the stake. - origin_netuid: Network UID of the origin subnet. - destination_netuid: Network UID of the destination subnet. - amount: The amount of stake to transfer as a `Balance` object. + subtensor: Subtensor instance. + wallet: The wallet to move stake from. + origin_netuid: The netuid of the source subnet. + origin_hotkey_ss58: The SS58 address of the source hotkey. + destination_netuid: The netuid of the destination subnet. + destination_hotkey_ss58: The SS58 address of the destination hotkey. + amount: Amount to move. + move_all_stake: If true, moves all stake from the source hotkey to the destination hotkey. 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. @@ -78,44 +81,50 @@ async def transfer_stake_extrinsic( ).success: return unlocked - amount.set_unit(netuid=origin_netuid) + if not amount and not move_all_stake: + return ExtrinsicResponse( + False, + "Please specify an `amount` or `move_all_stake` argument to move stake.", + ).with_log() # Check sufficient stake stake_in_origin, stake_in_destination = await _get_stake_in_origin_and_dest( subtensor=subtensor, - origin_hotkey_ss58=hotkey_ss58, - destination_hotkey_ss58=hotkey_ss58, - origin_coldkey_ss58=wallet.coldkeypub.ss58_address, - destination_coldkey_ss58=destination_coldkey_ss58, origin_netuid=origin_netuid, + origin_hotkey_ss58=origin_hotkey_ss58, destination_netuid=destination_netuid, + destination_hotkey_ss58=destination_hotkey_ss58, + origin_coldkey_ss58=wallet.coldkeypub.ss58_address, + destination_coldkey_ss58=wallet.coldkeypub.ss58_address, ) - if stake_in_origin < amount: + if move_all_stake: + amount = stake_in_origin + + elif stake_in_origin < amount: return ExtrinsicResponse( False, - f"Insufficient stake in origin hotkey: {hotkey_ss58}. Stake: {stake_in_origin}, amount: {amount}.", + f"Insufficient stake in origin hotkey: {origin_hotkey_ss58}. Stake: {stake_in_origin}, amount: {amount}.", ).with_log() + amount.set_unit(netuid=origin_netuid) + logging.debug( - f"Transferring stake from coldkey [blue]{wallet.coldkeypub.ss58_address}[/blue] to coldkey " - f"[blue]{destination_coldkey_ss58}[/blue]" - ) - logging.debug( + f"Moving stake from hotkey [blue]{origin_hotkey_ss58}[/blue] to hotkey [blue]{destination_hotkey_ss58}[/blue]\n" f"Amount: [green]{amount}[/green] from netuid [yellow]{origin_netuid}[/yellow] to netuid " f"[yellow]{destination_netuid}[/yellow]" ) - call = await subtensor.substrate.compose_call( + call = await subtensor.compose_call( call_module="SubtensorModule", - call_function="transfer_stake", - call_params={ - "destination_coldkey": destination_coldkey_ss58, - "hotkey": hotkey_ss58, - "origin_netuid": origin_netuid, - "destination_netuid": destination_netuid, - "alpha_amount": amount.rao, - }, + call_function="move_stake", + call_params=MoveStakeParams.move_stake( + origin_netuid=origin_netuid, + origin_hotkey_ss58=origin_hotkey_ss58, + destination_netuid=destination_netuid, + destination_hotkey_ss58=destination_hotkey_ss58, + amount=amount, + ), ) - + block_hash_before = await subtensor.get_block_hash() response = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, @@ -126,16 +135,27 @@ async def transfer_stake_extrinsic( ) if response.success: + sim_swap = await subtensor.sim_swap( + origin_netuid=origin_netuid, + destination_netuid=destination_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(origin_netuid) + if not wait_for_finalization and not wait_for_inclusion: return response + logging.debug("[green]Finalized[/green]") + # Get updated stakes origin_stake, dest_stake = await _get_stake_in_origin_and_dest( subtensor=subtensor, - origin_hotkey_ss58=hotkey_ss58, - destination_hotkey_ss58=hotkey_ss58, + origin_hotkey_ss58=origin_hotkey_ss58, + destination_hotkey_ss58=destination_hotkey_ss58, origin_coldkey_ss58=wallet.coldkeypub.ss58_address, - destination_coldkey_ss58=destination_coldkey_ss58, + destination_coldkey_ss58=wallet.coldkeypub.ss58_address, origin_netuid=origin_netuid, destination_netuid=destination_netuid, ) @@ -146,6 +166,12 @@ async def transfer_stake_extrinsic( f"Destination Stake: [blue]{stake_in_destination}[/blue] :arrow_right: [green]{dest_stake}[/green]" ) + response.data = { + "origin_stake_before": stake_in_origin, + "origin_stake_after": origin_stake, + "destination_stake_before": stake_in_destination, + "destination_stake_after": dest_stake, + } return response logging.error(f"[red]{response.message}[/red]") @@ -155,34 +181,30 @@ async def transfer_stake_extrinsic( return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) -async def swap_stake_extrinsic( +async def transfer_stake_extrinsic( subtensor: "AsyncSubtensor", wallet: "Wallet", + destination_coldkey_ss58: str, hotkey_ss58: str, origin_netuid: int, destination_netuid: int, amount: Balance, - safe_swapping: bool = False, - allow_partial_stake: bool = False, - rate_tolerance: float = 0.005, period: Optional[int] = None, raise_error: bool = False, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, ) -> ExtrinsicResponse: """ - Swaps stake from one subnet to another for a given hotkey in the Bittensor network. + Transfers stake from one coldkey to another in the Bittensor network. Parameters: - subtensor: Subtensor instance. - wallet: The wallet to swap stake from. - hotkey_ss58: The hotkey SS58 address associated with the stake. - origin_netuid: The source subnet UID. - destination_netuid: The destination subnet UID. - amount: Amount to swap. - safe_swapping: If true, enables price safety checks to protect against price impact. - allow_partial_stake: If true, allows partial stake swaps when the full amount would exceed the price tolerance. - rate_tolerance: Maximum allowed increase in a price ratio (0.005 = 0.5%). + subtensor: The subtensor instance to interact with the blockchain. + wallet: The wallet containing the coldkey to authorize the transfer. + destination_coldkey_ss58: SS58 address of the destination coldkey. + hotkey_ss58: SS58 address of the hotkey associated with the stake. + origin_netuid: Network UID of the origin subnet. + destination_netuid: Network UID of the destination subnet. + amount: The amount of stake to transfer as a `Balance` object. 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. @@ -190,11 +212,9 @@ async def swap_stake_extrinsic( wait_for_inclusion: Whether to wait for the inclusion of the transaction. wait_for_finalization: Whether to wait for the finalization of the transaction. - Returns: ExtrinsicResponse: The result object of the extrinsic execution. """ - try: if not ( unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) @@ -209,60 +229,36 @@ async def swap_stake_extrinsic( origin_hotkey_ss58=hotkey_ss58, destination_hotkey_ss58=hotkey_ss58, origin_coldkey_ss58=wallet.coldkeypub.ss58_address, - destination_coldkey_ss58=wallet.coldkeypub.ss58_address, + destination_coldkey_ss58=destination_coldkey_ss58, origin_netuid=origin_netuid, destination_netuid=destination_netuid, ) - if stake_in_origin < amount: return ExtrinsicResponse( False, f"Insufficient stake in origin hotkey: {hotkey_ss58}. Stake: {stake_in_origin}, amount: {amount}.", ).with_log() - call_params = { - "hotkey": hotkey_ss58, - "origin_netuid": origin_netuid, - "destination_netuid": destination_netuid, - "alpha_amount": amount.rao, - } - - if safe_swapping: - 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.debug( - 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.debug( - 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( + logging.debug( + f"Transferring stake from coldkey [blue]{wallet.coldkeypub.ss58_address}[/blue] to coldkey " + f"[blue]{destination_coldkey_ss58}[/blue]" + ) + logging.debug( + f"Amount: [green]{amount}[/green] from netuid [yellow]{origin_netuid}[/yellow] to netuid " + f"[yellow]{destination_netuid}[/yellow]" + ) + call = await subtensor.compose_call( call_module="SubtensorModule", - call_function=call_function, - call_params=call_params, + call_function="transfer_stake", + call_params=MoveStakeParams.transfer_stake( + hotkey_ss58=hotkey_ss58, + origin_netuid=origin_netuid, + destination_coldkey_ss58=destination_coldkey_ss58, + destination_netuid=destination_netuid, + amount=amount, + ), ) - + block_hash_before = await subtensor.get_block_hash() response = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, @@ -273,22 +269,28 @@ async def swap_stake_extrinsic( ) if response.success: + sim_swap = await subtensor.sim_swap( + origin_netuid=origin_netuid, + destination_netuid=destination_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(origin_netuid) + if not wait_for_finalization and not wait_for_inclusion: return response - logging.debug("[green]Finalized[/green]") - # Get updated stakes origin_stake, dest_stake = await _get_stake_in_origin_and_dest( - subtensor, + subtensor=subtensor, origin_hotkey_ss58=hotkey_ss58, destination_hotkey_ss58=hotkey_ss58, origin_coldkey_ss58=wallet.coldkeypub.ss58_address, - destination_coldkey_ss58=wallet.coldkeypub.ss58_address, + destination_coldkey_ss58=destination_coldkey_ss58, origin_netuid=origin_netuid, destination_netuid=destination_netuid, ) - logging.debug( f"Origin Stake: [blue]{stake_in_origin}[/blue] :arrow_right: [green]{origin_stake}[/green]" ) @@ -296,17 +298,8 @@ async def swap_stake_extrinsic( f"Destination Stake: [blue]{stake_in_destination}[/blue] :arrow_right: [green]{dest_stake}[/green]" ) - response.data = { - "origin_stake_before": stake_in_origin, - "origin_stake_after": origin_stake, - "destination_stake_before": stake_in_destination, - "destination_stake_after": dest_stake, - } return response - if safe_swapping and "Custom error: 8" in response.message: - response.message = "Price ratio exceeded tolerance limit. Either increase price tolerance or enable partial staking." - logging.error(f"[red]{response.message}[/red]") return response @@ -314,32 +307,34 @@ async def swap_stake_extrinsic( return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) -async def move_stake_extrinsic( +async def swap_stake_extrinsic( subtensor: "AsyncSubtensor", wallet: "Wallet", - origin_hotkey_ss58: str, + hotkey_ss58: str, origin_netuid: int, - destination_hotkey_ss58: str, destination_netuid: int, - amount: Optional[Balance] = None, - move_all_stake: bool = False, + amount: Balance, + safe_swapping: bool = False, + allow_partial_stake: bool = False, + rate_tolerance: float = 0.005, period: Optional[int] = None, raise_error: bool = False, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, ) -> ExtrinsicResponse: """ - Moves stake from one hotkey to another within subnets in the Bittensor network. + Swaps stake from one subnet to another for a given hotkey in the Bittensor network. Parameters: subtensor: Subtensor instance. - wallet: The wallet to move stake from. - origin_hotkey_ss58: The SS58 address of the source hotkey. - origin_netuid: The netuid of the source subnet. - destination_hotkey_ss58: The SS58 address of the destination hotkey. - destination_netuid: The netuid of the destination subnet. - amount: Amount to move. - move_all_stake: If true, moves all stake from the source hotkey to the destination hotkey. + wallet: The wallet to swap stake from. + hotkey_ss58: The hotkey SS58 address associated with the stake. + origin_netuid: The source subnet UID. + destination_netuid: The destination subnet UID. + amount: Amount to swap. + safe_swapping: If true, enables price safety checks to protect against price impact. + allow_partial_stake: If true, allows partial stake swaps when the full amount would exceed the price tolerance. + rate_tolerance: Maximum allowed increase in a price ratio (0.005 = 0.5%). 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. @@ -347,59 +342,90 @@ async def move_stake_extrinsic( wait_for_inclusion: Whether to wait for the inclusion of the transaction. wait_for_finalization: Whether to wait for the finalization of the transaction. + Returns: ExtrinsicResponse: The result object of the extrinsic execution. """ + try: if not ( unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) ).success: return unlocked - if not amount and not move_all_stake: - return ExtrinsicResponse( - False, - "Please specify an `amount` or `move_all_stake` argument to move stake.", - ).with_log() + amount.set_unit(netuid=origin_netuid) # Check sufficient stake stake_in_origin, stake_in_destination = await _get_stake_in_origin_and_dest( subtensor=subtensor, - origin_hotkey_ss58=origin_hotkey_ss58, - destination_hotkey_ss58=destination_hotkey_ss58, - origin_netuid=origin_netuid, - destination_netuid=destination_netuid, + origin_hotkey_ss58=hotkey_ss58, + destination_hotkey_ss58=hotkey_ss58, origin_coldkey_ss58=wallet.coldkeypub.ss58_address, destination_coldkey_ss58=wallet.coldkeypub.ss58_address, + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, ) - if move_all_stake: - amount = stake_in_origin - elif stake_in_origin < amount: + if stake_in_origin < amount: return ExtrinsicResponse( False, - f"Insufficient stake in origin hotkey: {origin_hotkey_ss58}. Stake: {stake_in_origin}, amount: {amount}.", + f"Insufficient stake in origin hotkey: {hotkey_ss58}. Stake: {stake_in_origin}, amount: {amount}.", ).with_log() - amount.set_unit(netuid=origin_netuid) + if safe_swapping: + 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.debug( - f"Moving stake from hotkey [blue]{origin_hotkey_ss58}[/blue] to hotkey [blue]{destination_hotkey_ss58}[/blue]\n" - f"Amount: [green]{amount}[/green] from netuid [yellow]{origin_netuid}[/yellow] to netuid " - f"[yellow]{destination_netuid}[/yellow]" - ) - call = await subtensor.substrate.compose_call( + call_function = "swap_stake_limit" + call_params = MoveStakeParams.swap_stake_limit( + hotkey_ss58=hotkey_ss58, + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + amount=amount, + allow_partial_stake=allow_partial_stake, + rate_tolerance=rate_tolerance, + origin_pool=origin_pool, + destination_pool=destination_pool, + ) + + logging.debug( + 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, + } + ) + + else: + call_function = "swap_stake" + call_params = MoveStakeParams.swap_stake( + hotkey_ss58=hotkey_ss58, + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + amount=amount, + ) + logging.debug( + 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 = await subtensor.compose_call( call_module="SubtensorModule", - call_function="move_stake", - call_params={ - "origin_hotkey": origin_hotkey_ss58, - "origin_netuid": origin_netuid, - "destination_hotkey": destination_hotkey_ss58, - "destination_netuid": destination_netuid, - "alpha_amount": amount.rao, - }, + call_function=call_function, + call_params=call_params, ) - + block_hash_before = await subtensor.get_block_hash() response = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, @@ -410,6 +436,15 @@ async def move_stake_extrinsic( ) if response.success: + sim_swap = await subtensor.sim_swap( + origin_netuid=origin_netuid, + destination_netuid=destination_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(origin_netuid) + if not wait_for_finalization and not wait_for_inclusion: return response @@ -418,13 +453,14 @@ async def move_stake_extrinsic( # Get updated stakes origin_stake, dest_stake = await _get_stake_in_origin_and_dest( subtensor=subtensor, - origin_hotkey_ss58=origin_hotkey_ss58, - destination_hotkey_ss58=destination_hotkey_ss58, + origin_hotkey_ss58=hotkey_ss58, + destination_hotkey_ss58=hotkey_ss58, origin_coldkey_ss58=wallet.coldkeypub.ss58_address, destination_coldkey_ss58=wallet.coldkeypub.ss58_address, origin_netuid=origin_netuid, destination_netuid=destination_netuid, ) + logging.debug( f"Origin Stake: [blue]{stake_in_origin}[/blue] :arrow_right: [green]{origin_stake}[/green]" ) @@ -440,6 +476,9 @@ async def move_stake_extrinsic( } return response + if safe_swapping and "Custom error: 8" in response.message: + response.message = "Price ratio exceeded tolerance limit. Either increase price tolerance or enable partial staking." + logging.error(f"[red]{response.message}[/red]") return response diff --git a/bittensor/core/extrinsics/asyncex/registration.py b/bittensor/core/extrinsics/asyncex/registration.py index 7b455d087b..ccc9d602cb 100644 --- a/bittensor/core/extrinsics/asyncex/registration.py +++ b/bittensor/core/extrinsics/asyncex/registration.py @@ -6,6 +6,7 @@ from typing import Optional, Union, TYPE_CHECKING from bittensor.core.errors import RegistrationError +from bittensor.core.extrinsics.params import RegistrationParams from bittensor.core.types import ExtrinsicResponse from bittensor.utils.btlogging import logging from bittensor.utils.registration import create_pow_async, log_no_torch_error, torch @@ -42,22 +43,26 @@ async def burned_register_extrinsic( """ try: if not ( - unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + unlocked := ExtrinsicResponse.unlock_wallet( + wallet, raise_error, unlock_type="both" + ) ).success: return unlocked block_hash = await subtensor.substrate.get_chain_head() - if not await subtensor.subnet_exists(netuid, block_hash=block_hash): + if not await subtensor.subnet_exists(netuid=netuid, block_hash=block_hash): return ExtrinsicResponse( False, f"Subnet {netuid} does not exist." ).with_log() neuron, old_balance, recycle_amount = await asyncio.gather( subtensor.get_neuron_for_pubkey_and_subnet( - wallet.hotkey.ss58_address, netuid=netuid, block_hash=block_hash + netuid=netuid, + hotkey_ss58=wallet.hotkey.ss58_address, + block_hash=block_hash, ), subtensor.get_balance( - wallet.coldkeypub.ss58_address, block_hash=block_hash + address=wallet.coldkeypub.ss58_address, block_hash=block_hash ), subtensor.recycle(netuid=netuid, block_hash=block_hash), ) @@ -75,13 +80,12 @@ async def burned_register_extrinsic( logging.debug(f"Recycling {recycle_amount} to register on subnet:{netuid}") - call = await subtensor.substrate.compose_call( + call = await subtensor.compose_call( call_module="SubtensorModule", call_function="burned_register", - call_params={ - "netuid": netuid, - "hotkey": wallet.hotkey.ss58_address, - }, + call_params=RegistrationParams.burned_register( + netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address + ), ) response = await subtensor.sign_and_send_extrinsic( call=call, @@ -101,7 +105,9 @@ async def burned_register_extrinsic( return response # Successful registration, final check for neuron and pubkey - new_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) + new_balance = await subtensor.get_balance( + address=wallet.coldkeypub.ss58_address + ) logging.debug( f"Balance: [blue]{old_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" @@ -160,7 +166,9 @@ async def register_subnet_extrinsic( """ try: if not ( - unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + unlocked := ExtrinsicResponse.unlock_wallet( + wallet, raise_error, unlock_type="both" + ) ).success: return unlocked @@ -173,12 +181,12 @@ async def register_subnet_extrinsic( f"Insufficient balance {balance} to register subnet. Current burn cost is {burn_cost} TAO.", ).with_log() - call = await subtensor.substrate.compose_call( + call = await subtensor.compose_call( call_module="SubtensorModule", call_function="register_network", - call_params={ - "hotkey": wallet.hotkey.ss58_address, - }, + call_params=RegistrationParams.register_network( + hotkey_ss58=wallet.hotkey.ss58_address + ), ) response = await subtensor.sign_and_send_extrinsic( @@ -250,7 +258,9 @@ async def register_extrinsic( """ try: if not ( - unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + unlocked := ExtrinsicResponse.unlock_wallet( + wallet, raise_error, unlock_type="both" + ) ).success: return unlocked @@ -332,17 +342,17 @@ async def register_extrinsic( else: # check if a pow result is still valid while not await pow_result.is_stale_async(subtensor=subtensor): - call = await subtensor.substrate.compose_call( + call = await subtensor.compose_call( call_module="SubtensorModule", call_function="register", - call_params={ - "netuid": netuid, - "block_number": pow_result.block_number, - "nonce": pow_result.nonce, - "work": [int(byte_) for byte_ in pow_result.seal], - "hotkey": wallet.hotkey.ss58_address, - "coldkey": wallet.coldkeypub.ss58_address, - }, + call_params=RegistrationParams.register( + netuid=netuid, + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=wallet.hotkey.ss58_address, + block_number=pow_result.block_number, + nonce=pow_result.nonce, + work=[int(byte_) for byte_ in pow_result.seal], + ), ) response = await subtensor.sign_and_send_extrinsic( call=call, @@ -437,25 +447,27 @@ async def set_subnet_identity_extrinsic( """ try: if not ( - unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + unlocked := ExtrinsicResponse.unlock_wallet( + wallet, raise_error, unlock_type="both" + ) ).success: return unlocked - call = await subtensor.substrate.compose_call( + call = await subtensor.compose_call( call_module="SubtensorModule", call_function="set_subnet_identity", - call_params={ - "hotkey": wallet.hotkey.ss58_address, - "netuid": netuid, - "subnet_name": subnet_name, - "github_repo": github_repo, - "subnet_contact": subnet_contact, - "subnet_url": subnet_url, - "logo_url": logo_url, - "discord": discord, - "description": description, - "additional": additional, - }, + call_params=RegistrationParams.set_subnet_identity( + hotkey_ss58=wallet.hotkey.ss58_address, + netuid=netuid, + subnet_name=subnet_name, + github_repo=github_repo, + subnet_contact=subnet_contact, + subnet_url=subnet_url, + logo_url=logo_url, + discord=discord, + description=description, + additional=additional, + ), ) response = await subtensor.sign_and_send_extrinsic( diff --git a/bittensor/core/extrinsics/asyncex/root.py b/bittensor/core/extrinsics/asyncex/root.py index cae2149052..45ea91878e 100644 --- a/bittensor/core/extrinsics/asyncex/root.py +++ b/bittensor/core/extrinsics/asyncex/root.py @@ -1,6 +1,7 @@ import asyncio from typing import Optional, TYPE_CHECKING +from bittensor.core.extrinsics.params import RootParams from bittensor.core.types import ExtrinsicResponse from bittensor.utils import u16_normalized_float from bittensor.utils.balance import Balance @@ -61,7 +62,9 @@ async def root_register_extrinsic( """ try: if not ( - unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + unlocked := ExtrinsicResponse.unlock_wallet( + wallet, raise_error, unlock_type="both" + ) ).success: return unlocked @@ -100,10 +103,10 @@ async def root_register_extrinsic( if is_registered: return ExtrinsicResponse(message="Already registered on root network.") - call = await subtensor.substrate.compose_call( + call = await subtensor.compose_call( call_module="SubtensorModule", call_function="root_register", - call_params={"hotkey": wallet.hotkey.ss58_address}, + call_params=RootParams.root_register(wallet.hotkey.ss58_address), ) response = await subtensor.sign_and_send_extrinsic( call=call, diff --git a/bittensor/core/extrinsics/asyncex/serving.py b/bittensor/core/extrinsics/asyncex/serving.py index 98c9938db6..71868f0381 100644 --- a/bittensor/core/extrinsics/asyncex/serving.py +++ b/bittensor/core/extrinsics/asyncex/serving.py @@ -2,8 +2,8 @@ from typing import Optional, Union, TYPE_CHECKING from bittensor.core.errors import MetadataError -from bittensor.core.settings import version_as_int -from bittensor.core.types import AxonServeCallParams, ExtrinsicResponse +from bittensor.core.extrinsics.params.serving import ServingParams +from bittensor.core.types import ExtrinsicResponse from bittensor.utils import ( networking as net, Certificate, @@ -11,9 +11,9 @@ from bittensor.utils.btlogging import logging if TYPE_CHECKING: + from bittensor_wallet import Wallet from bittensor.core.axon import Axon from bittensor.core.async_subtensor import AsyncSubtensor - from bittensor_wallet import Wallet async def serve_extrinsic( @@ -55,29 +55,25 @@ async def serve_extrinsic( ExtrinsicResponse: The result object of the extrinsic execution. """ try: - signing_keypair = "hotkey" if not ( unlocked := ExtrinsicResponse.unlock_wallet( - wallet, raise_error, signing_keypair + wallet, raise_error, unlock_type="both" ) ).success: return unlocked - params = AxonServeCallParams( - **{ - "version": version_as_int, - "ip": net.ip_to_int(ip), - "port": port, - "ip_type": net.ip_version(ip), - "netuid": netuid, - "hotkey": wallet.hotkey.ss58_address, - "coldkey": wallet.coldkeypub.ss58_address, - "protocol": protocol, - "placeholder1": placeholder1, - "placeholder2": placeholder2, - "certificate": certificate, - } + params = ServingParams.serve_axon_and_tls( + hotkey_ss58=wallet.hotkey.ss58_address, + coldkey_ss58=wallet.coldkeypub.ss58_address, + netuid=netuid, + ip=ip, + port=port, + protocol=protocol, + placeholder1=placeholder1, + placeholder2=placeholder2, + certificate=certificate, ) + logging.debug("Checking axon ...") neuron = await subtensor.get_neuron_for_pubkey_and_subnet( wallet.hotkey.ss58_address, netuid=netuid @@ -98,7 +94,7 @@ async def serve_extrinsic( else: call_function = "serve_axon_tls" - call = await subtensor.substrate.compose_call( + call = await subtensor.compose_call( call_module="SubtensorModule", call_function=call_function, call_params=params.dict(), @@ -108,7 +104,7 @@ async def serve_extrinsic( wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, - sign_with=signing_keypair, + sign_with="hotkey", period=period, raise_error=raise_error, ) @@ -156,13 +152,6 @@ async def serve_axon_extrinsic( ExtrinsicResponse: The result object of the extrinsic execution. """ try: - if not ( - unlocked := ExtrinsicResponse.unlock_wallet( - axon.wallet, raise_error, "hotkey" - ) - ).success: - return unlocked - external_port = axon.external_port # ---- Get external ip ---- @@ -260,13 +249,10 @@ async def publish_metadata_extrinsic( if reset_bonds: fields.append({"ResetBondsFlag": b""}) - call = await subtensor.substrate.compose_call( + call = await subtensor.compose_call( call_module="Commitments", call_function="set_commitment", - call_params={ - "netuid": netuid, - "info": {"fields": [fields]}, - }, + call_params=ServingParams.set_commitment(netuid=netuid, info_fields=fields), ) response = await subtensor.sign_and_send_extrinsic( diff --git a/bittensor/core/extrinsics/asyncex/staking.py b/bittensor/core/extrinsics/asyncex/staking.py index 0aacf21c64..7de816e596 100644 --- a/bittensor/core/extrinsics/asyncex/staking.py +++ b/bittensor/core/extrinsics/asyncex/staking.py @@ -4,6 +4,7 @@ from async_substrate_interface.errors import SubstrateRequestException from bittensor.core.errors import BalanceTypeError +from bittensor.core.extrinsics.params import StakingParams from bittensor.core.extrinsics.utils import get_old_stakes from bittensor.core.types import ExtrinsicResponse, UIDs from bittensor.utils import format_error_message @@ -95,49 +96,45 @@ async def add_stake_extrinsic( logging.debug(f"\t\twallet: {wallet.name}") return ExtrinsicResponse(False, f"{message}.").with_log() - call_params = { - "hotkey": hotkey_ss58, - "netuid": netuid, - "amount_staked": amount.rao, - } - if safe_staking: pool = await subtensor.subnet(netuid=netuid) - base_price = pool.price.tao - price_with_tolerance = ( - base_price if pool.netuid == 0 else base_price * (1 + rate_tolerance) + call_function = "add_stake_limit" + call_params = StakingParams.add_stake_limit( + netuid=netuid, + hotkey_ss58=hotkey_ss58, + amount=amount, + allow_partial_stake=allow_partial_stake, + rate_tolerance=rate_tolerance, + pool=pool, ) logging.debug( f"Safe Staking to: [blue]netuid: [green]{netuid}[/green], amount: [green]{amount}[/green], " f"tolerance percentage: [green]{rate_tolerance * 100}%[/green], " - f"price limit: [green]{price_with_tolerance}[/green], " - f"original price: [green]{base_price}[/green], " + f"price limit: [green]{call_params.get('limit_price')}[/green], " + f"original price: [green]{pool.price}[/green], " f"with partial stake: [green]{allow_partial_stake}[/green] " f"on [blue]{subtensor.network}[/blue]." ) - limit_price = Balance.from_tao(price_with_tolerance).rao - call_params.update( - { - "limit_price": limit_price, - "allow_partial": allow_partial_stake, - } - ) - call_function = "add_stake_limit" else: + call_function = "add_stake" + call_params = StakingParams.add_stake( + netuid=netuid, hotkey_ss58=hotkey_ss58, amount=amount + ) + logging.debug( f"Staking to: [blue]netuid: [green]{netuid}[/green], amount: [green]{amount}[/green] " f"on [blue]{subtensor.network}[/blue]." ) - call_function = "add_stake" - call = await subtensor.substrate.compose_call( + call = await subtensor.compose_call( call_module="SubtensorModule", call_function=call_function, call_params=call_params, ) + block_hash_before = await subtensor.get_block_hash() response = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, @@ -149,6 +146,15 @@ async def add_stake_extrinsic( 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]") @@ -446,13 +452,12 @@ async def set_auto_stake_extrinsic( ).success: return unlocked - call = await subtensor.substrate.compose_call( + call = await subtensor.compose_call( call_module="SubtensorModule", call_function="set_coldkey_auto_stake_hotkey", - call_params={ - "netuid": netuid, - "hotkey": hotkey_ss58, - }, + call_params=StakingParams.set_coldkey_auto_stake_hotkey( + netuid, hotkey_ss58 + ), ) response = await subtensor.sign_and_send_extrinsic( call=call, diff --git a/bittensor/core/extrinsics/asyncex/start_call.py b/bittensor/core/extrinsics/asyncex/start_call.py index f40f8e77e1..7b933eec10 100644 --- a/bittensor/core/extrinsics/asyncex/start_call.py +++ b/bittensor/core/extrinsics/asyncex/start_call.py @@ -1,5 +1,6 @@ from typing import TYPE_CHECKING, Optional +from bittensor.core.extrinsics.params import StartCallParams from bittensor.core.types import ExtrinsicResponse if TYPE_CHECKING: @@ -40,21 +41,20 @@ async def start_call_extrinsic( ).success: return unlocked - async with subtensor.substrate as substrate: - start_call = await substrate.compose_call( - call_module="SubtensorModule", - call_function="start_call", - call_params={"netuid": netuid}, - ) - - return await subtensor.sign_and_send_extrinsic( - call=start_call, - wallet=wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - period=period, - raise_error=raise_error, - ) + call = await subtensor.compose_call( + call_module="SubtensorModule", + call_function="start_call", + call_params=StartCallParams.start_call(netuid=netuid), + ) + + return await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + raise_error=raise_error, + ) except Exception as error: return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) diff --git a/bittensor/core/extrinsics/asyncex/take.py b/bittensor/core/extrinsics/asyncex/take.py index 96416e18c2..f5fadf4111 100644 --- a/bittensor/core/extrinsics/asyncex/take.py +++ b/bittensor/core/extrinsics/asyncex/take.py @@ -2,6 +2,7 @@ from bittensor_wallet.bittensor_wallet import Wallet +from bittensor.core.extrinsics.params import TakeParams from bittensor.core.types import ExtrinsicResponse if TYPE_CHECKING: @@ -43,13 +44,10 @@ async def set_take_extrinsic( ).success: return unlocked - call = await subtensor.substrate.compose_call( + call = await subtensor.compose_call( call_module="SubtensorModule", call_function=action, - call_params={ - "hotkey": hotkey_ss58, - "take": take, - }, + call_params=TakeParams.increase_decrease_take(hotkey_ss58, take), ) return await subtensor.sign_and_send_extrinsic( call=call, diff --git a/bittensor/core/extrinsics/asyncex/transfer.py b/bittensor/core/extrinsics/asyncex/transfer.py index 3d4b718dfc..7e5eea288f 100644 --- a/bittensor/core/extrinsics/asyncex/transfer.py +++ b/bittensor/core/extrinsics/asyncex/transfer.py @@ -1,11 +1,11 @@ import asyncio from typing import TYPE_CHECKING, Optional +from bittensor.core.extrinsics.params import get_transfer_fn_params from bittensor.core.settings import NETWORK_EXPLORER_MAP, DEFAULT_NETWORK from bittensor.core.types import ExtrinsicResponse from bittensor.utils import ( get_explorer_url_for_network, - get_transfer_fn_params, is_valid_bittensor_address_or_public_key, ) from bittensor.utils.balance import Balance @@ -100,7 +100,7 @@ async def transfer_extrinsic( amount, destination, keep_alive ) - call = await subtensor.substrate.compose_call( + call = await subtensor.compose_call( call_module="Balances", call_function=call_function, call_params=call_params, @@ -114,7 +114,7 @@ async def transfer_extrinsic( period=period, raise_error=raise_error, ) - response.transaction_fee = fee + response.transaction_tao_fee = fee if response.success: block_hash = await subtensor.get_block_hash() diff --git a/bittensor/core/extrinsics/asyncex/unstaking.py b/bittensor/core/extrinsics/asyncex/unstaking.py index d666716b56..478f95e646 100644 --- a/bittensor/core/extrinsics/asyncex/unstaking.py +++ b/bittensor/core/extrinsics/asyncex/unstaking.py @@ -4,6 +4,7 @@ from async_substrate_interface.errors import SubstrateRequestException from bittensor.core.errors import BalanceTypeError +from bittensor.core.extrinsics.params import UnstakingParams from bittensor.core.extrinsics.utils import get_old_stakes from bittensor.core.types import ExtrinsicResponse from bittensor.core.types import UIDs @@ -80,53 +81,50 @@ async def unstake_extrinsic( f"Not enough stake: {old_stake} to unstake: {amount} from hotkey: {hotkey_ss58}", ).with_log() - call_params = { - "hotkey": hotkey_ss58, - "netuid": netuid, - "amount_unstaked": amount.rao, - } if safe_unstaking: pool = await subtensor.subnet(netuid=netuid) - base_price = pool.price.tao - if pool.netuid == 0: - price_with_tolerance = base_price - else: - price_with_tolerance = base_price * (1 - rate_tolerance) + call_function = "remove_stake_limit" + call_params = UnstakingParams.remove_stake_limit( + netuid=netuid, + hotkey_ss58=hotkey_ss58, + amount=amount, + allow_partial_stake=allow_partial_stake, + rate_tolerance=rate_tolerance, + pool=pool, + ) logging_message = ( f"Safe Unstaking from: " f"netuid: [green]{netuid}[/green], amount: [green]{amount}[/green], " f"tolerance percentage: [green]{rate_tolerance * 100}%[/green], " - f"price limit: [green]{price_with_tolerance}[/green], " - f"original price: [green]{base_price}[/green], " + f"price limit: [green]{Balance.from_rao(call_params['limit_price'])}[/green], " + f"original price: [green]{pool.price.tao}[/green], " f"with partial unstake: [green]{allow_partial_stake}[/green] " f"on [blue]{subtensor.network}[/blue]" ) - limit_price = Balance.from_tao(price_with_tolerance).rao - call_params.update( - { - "limit_price": limit_price, - "allow_partial": allow_partial_stake, - } - ) - call_function = "remove_stake_limit" else: + call_function = "remove_stake" + call_params = UnstakingParams.remove_stake( + netuid=netuid, + hotkey_ss58=hotkey_ss58, + amount=amount, + ) logging_message = ( - f"Unstaking from: " + f":satellite: [magenta]Unstaking from:[/magenta] " f"netuid: [green]{netuid}[/green], amount: [green]{amount}[/green] " f"on [blue]{subtensor.network}[/blue]" ) - call_function = "remove_stake" logging.debug(logging_message) - call = await subtensor.substrate.compose_call( + call = await subtensor.compose_call( call_module="SubtensorModule", call_function=call_function, call_params=call_params, ) + block_hash_before = await subtensor.get_block_hash() response = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, @@ -138,8 +136,16 @@ async def unstake_extrinsic( raise_error=raise_error, ) - if response.success: # If we successfully unstaked. - # We only wait here if we expect finalization. + if response.success: + sim_swap = await subtensor.sim_swap( + origin_netuid=netuid, + destination_netuid=0, + 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 @@ -184,8 +190,8 @@ async def unstake_extrinsic( async def unstake_all_extrinsic( subtensor: "AsyncSubtensor", wallet: "Wallet", - hotkey_ss58: str, netuid: int, + hotkey_ss58: str, rate_tolerance: Optional[float] = 0.005, period: Optional[int] = None, raise_error: bool = False, @@ -197,8 +203,8 @@ async def unstake_all_extrinsic( Parameters: subtensor: Subtensor instance. wallet: The wallet of the stake owner. - hotkey_ss58: The SS58 address of the hotkey to unstake from. netuid: The unique identifier of the subnet. + hotkey_ss58: The SS58 address of the hotkey to unstake from. rate_tolerance: The maximum allowed price change ratio when unstaking. For example, 0.005 = 0.5% maximum price decrease. If not passed (None), then unstaking goes without price limit. period: The number of blocks during which the transaction will remain valid after it's submitted. If the @@ -217,34 +223,30 @@ async def unstake_all_extrinsic( ).success: return unlocked - call_params = { - "hotkey": hotkey_ss58, - "netuid": netuid, - "limit_price": None, - } - - if rate_tolerance: - current_price = (await subtensor.subnet(netuid=netuid)).price - limit_price = current_price * (1 - rate_tolerance) - call_params.update({"limit_price": limit_price}) - - async with subtensor.substrate as substrate: - call = await substrate.compose_call( - call_module="SubtensorModule", - call_function="remove_stake_full_limit", - call_params=call_params, - ) + pool = await subtensor.subnet(netuid=netuid) if rate_tolerance else None + call_params = UnstakingParams.remove_stake_full_limit( + netuid=netuid, + hotkey_ss58=hotkey_ss58, + rate_tolerance=rate_tolerance, + pool=pool, + ) - return 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, - ) + call = await subtensor.compose_call( + call_module="SubtensorModule", + call_function="remove_stake_full_limit", + call_params=call_params, + ) + + return 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, + ) except Exception as error: return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) diff --git a/bittensor/core/extrinsics/asyncex/utils.py b/bittensor/core/extrinsics/asyncex/utils.py index f8fa39e794..a0a9fa2422 100644 --- a/bittensor/core/extrinsics/asyncex/utils.py +++ b/bittensor/core/extrinsics/asyncex/utils.py @@ -1,41 +1,12 @@ from typing import TYPE_CHECKING, Optional from bittensor.core.types import ExtrinsicResponse -from bittensor.utils.balance import Balance if TYPE_CHECKING: - from scalecodec import GenericCall - from bittensor_wallet import Keypair from bittensor.core.async_subtensor import AsyncSubtensor from bittensor_wallet import Wallet -async def get_extrinsic_fee( - subtensor: "AsyncSubtensor", - call: "GenericCall", - keypair: "Keypair", - netuid: Optional[int] = None, -): - """ - Get extrinsic fee for a given extrinsic call and keypair for a given SN's netuid. - - Parameters: - subtensor: The Subtensor instance. - netuid: The SN's netuid. - call: The extrinsic call. - keypair: The keypair associated with the extrinsic. - - Returns: - Balance object representing the extrinsic fee in RAO. - """ - payment_info = await subtensor.substrate.get_payment_info( - call=call, keypair=keypair - ) - return Balance.from_rao(amount=payment_info["partial_fee"]).set_unit( - netuid=netuid or 0 - ) - - async def sudo_call_extrinsic( subtensor: "AsyncSubtensor", wallet: "Wallet", @@ -81,13 +52,13 @@ async def sudo_call_extrinsic( ).success: return unlocked - call = await subtensor.substrate.compose_call( + call = await subtensor.compose_call( call_module=call_module, call_function=call_function, call_params=call_params, ) if not root_call: - call = await subtensor.substrate.compose_call( + call = await subtensor.compose_call( call_module="Sudo", call_function="sudo", call_params={"call": call}, diff --git a/bittensor/core/extrinsics/asyncex/weights.py b/bittensor/core/extrinsics/asyncex/weights.py index e0bda0effa..b6ab9eefba 100644 --- a/bittensor/core/extrinsics/asyncex/weights.py +++ b/bittensor/core/extrinsics/asyncex/weights.py @@ -4,6 +4,7 @@ from bittensor_drand import get_encrypted_commit +from bittensor.core.extrinsics.params import WeightsParams from bittensor.core.settings import version_as_int from bittensor.core.types import ExtrinsicResponse, Salt, UIDs, Weights from bittensor.utils import get_mechid_storage_index @@ -89,16 +90,16 @@ async def commit_timelocked_weights_extrinsic( hotkey=wallet.hotkey.public_key, ) - call = await subtensor.substrate.compose_call( + call = await subtensor.compose_call( call_module="SubtensorModule", call_function="commit_timelocked_mechanism_weights", - call_params={ - "netuid": netuid, - "mecid": mechid, - "commit": commit_for_reveal, - "reveal_round": reveal_round, - "commit_reveal_version": commit_reveal_version, - }, + call_params=WeightsParams.commit_timelocked_mechanism_weights( + netuid=netuid, + mechid=mechid, + commit_for_reveal=commit_for_reveal, + reveal_round=reveal_round, + commit_reveal_version=commit_reveal_version, + ), ) response = await subtensor.sign_and_send_extrinsic( call=call, @@ -182,14 +183,14 @@ async def commit_weights_extrinsic( version_key=version_key, ) - call = await subtensor.substrate.compose_call( + call = await subtensor.compose_call( call_module="SubtensorModule", call_function="commit_mechanism_weights", - call_params={ - "netuid": netuid, - "mecid": mechid, - "commit_hash": commit_hash, - }, + call_params=WeightsParams.commit_mechanism_weights( + netuid=netuid, + mechid=mechid, + commit_hash=commit_hash, + ), ) response = await subtensor.sign_and_send_extrinsic( call=call, @@ -262,17 +263,17 @@ async def reveal_weights_extrinsic( # Convert, reformat and normalize uids and weights. uids, weights = convert_and_normalize_weights_and_uids(uids, weights) - call = await subtensor.substrate.compose_call( + call = await subtensor.compose_call( call_module="SubtensorModule", call_function="reveal_mechanism_weights", - call_params={ - "netuid": netuid, - "mecid": mechid, - "uids": uids, - "values": weights, - "salt": salt, - "version_key": version_key, - }, + call_params=WeightsParams.reveal_mechanism_weights( + netuid=netuid, + mechid=mechid, + uids=uids, + weights=weights, + salt=salt, + version_key=version_key, + ), ) response = await subtensor.sign_and_send_extrinsic( call=call, @@ -343,16 +344,16 @@ async def set_weights_extrinsic( # Convert, reformat and normalize uids and weights. uids, weights = convert_and_normalize_weights_and_uids(uids, weights) - call = await subtensor.substrate.compose_call( + call = await subtensor.compose_call( call_module="SubtensorModule", call_function="set_mechanism_weights", - call_params={ - "netuid": netuid, - "mecid": mechid, - "dests": uids, - "weights": weights, - "version_key": version_key, - }, + call_params=WeightsParams.set_mechanism_weights( + netuid=netuid, + mechid=mechid, + uids=uids, + weights=weights, + version_key=version_key, + ), ) response = await subtensor.sign_and_send_extrinsic( call=call, diff --git a/bittensor/core/extrinsics/children.py b/bittensor/core/extrinsics/children.py index 1d81ea0a8a..31f241dd7b 100644 --- a/bittensor/core/extrinsics/children.py +++ b/bittensor/core/extrinsics/children.py @@ -1,8 +1,8 @@ from typing import TYPE_CHECKING, Optional -from bittensor.core.types import ExtrinsicResponse -from bittensor.utils import float_to_u64 +from bittensor.core.extrinsics.params import ChildrenParams from bittensor.core.extrinsics.utils import sudo_call_extrinsic +from bittensor.core.types import ExtrinsicResponse if TYPE_CHECKING: from bittensor_wallet import Wallet @@ -58,20 +58,10 @@ def set_children_extrinsic( ).success: return unlocked - call = subtensor.substrate.compose_call( + call = subtensor.compose_call( call_module="SubtensorModule", call_function="set_children", - call_params={ - "children": [ - ( - float_to_u64(proportion), - child_hotkey, - ) - for proportion, child_hotkey in children - ], - "hotkey": hotkey_ss58, - "netuid": netuid, - }, + call_params=ChildrenParams.set_children(hotkey_ss58, netuid, children), ) response = subtensor.sign_and_send_extrinsic( @@ -119,7 +109,7 @@ def root_set_pending_childkey_cooldown_extrinsic( wallet=wallet, call_module="SubtensorModule", call_function="set_pending_childkey_cooldown", - call_params={"cooldown": cooldown}, + call_params=ChildrenParams.set_pending_childkey_cooldown(cooldown), period=period, raise_error=raise_error, wait_for_inclusion=wait_for_inclusion, diff --git a/bittensor/core/extrinsics/liquidity.py b/bittensor/core/extrinsics/liquidity.py index f86604a202..e3d1fba25f 100644 --- a/bittensor/core/extrinsics/liquidity.py +++ b/bittensor/core/extrinsics/liquidity.py @@ -1,8 +1,8 @@ from typing import Optional, TYPE_CHECKING +from bittensor.core.extrinsics.params import LiquidityParams from bittensor.core.types import ExtrinsicResponse from bittensor.utils.balance import Balance -from bittensor.utils.liquidity import price_to_tick if TYPE_CHECKING: from bittensor_wallet import Wallet @@ -47,24 +47,26 @@ def add_liquidity_extrinsic( `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. """ try: + unlock_type = "coldkey" if hotkey_ss58 else "both" if not ( - unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + unlocked := ExtrinsicResponse.unlock_wallet( + wallet, raise_error, unlock_type + ) ).success: return unlocked - tick_low = price_to_tick(price_low.tao) - tick_high = price_to_tick(price_high.tao) + hotkey_ss58 = hotkey_ss58 or wallet.hotkey.ss58_address - call = subtensor.substrate.compose_call( + call = subtensor.compose_call( call_module="Swap", call_function="add_liquidity", - call_params={ - "hotkey": hotkey_ss58 or wallet.hotkey.ss58_address, - "netuid": netuid, - "tick_low": tick_low, - "tick_high": tick_high, - "liquidity": liquidity.rao, - }, + call_params=LiquidityParams.add_liquidity( + netuid=netuid, + hotkey_ss58=hotkey_ss58 or wallet.hotkey.ss58_address, + liquidity=liquidity, + price_low=price_low, + price_high=price_high, + ), ) return subtensor.sign_and_send_extrinsic( @@ -114,20 +116,23 @@ def modify_liquidity_extrinsic( `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. """ try: + unlock_type = "coldkey" if hotkey_ss58 else "both" if not ( - unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + unlocked := ExtrinsicResponse.unlock_wallet( + wallet, raise_error, unlock_type + ) ).success: return unlocked - call = subtensor.substrate.compose_call( + call = subtensor.compose_call( call_module="Swap", call_function="modify_position", - call_params={ - "hotkey": hotkey_ss58 or wallet.hotkey.ss58_address, - "netuid": netuid, - "position_id": position_id, - "liquidity_delta": liquidity_delta.rao, - }, + call_params=LiquidityParams.modify_position( + netuid=netuid, + hotkey_ss58=hotkey_ss58 or wallet.hotkey.ss58_address, + position_id=position_id, + liquidity_delta=liquidity_delta, + ), ) return subtensor.sign_and_send_extrinsic( @@ -175,19 +180,22 @@ def remove_liquidity_extrinsic( `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. """ try: + unlock_type = "coldkey" if hotkey_ss58 else "both" if not ( - unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + unlocked := ExtrinsicResponse.unlock_wallet( + wallet, raise_error, unlock_type + ) ).success: return unlocked - call = subtensor.substrate.compose_call( + call = subtensor.compose_call( call_module="Swap", call_function="remove_liquidity", - call_params={ - "hotkey": hotkey_ss58 or wallet.hotkey.ss58_address, - "netuid": netuid, - "position_id": position_id, - }, + call_params=LiquidityParams.remove_liquidity( + netuid=netuid, + hotkey_ss58=hotkey_ss58 or wallet.hotkey.ss58_address, + position_id=position_id, + ), ) return subtensor.sign_and_send_extrinsic( @@ -235,10 +243,13 @@ def toggle_user_liquidity_extrinsic( ).success: return unlocked - call = subtensor.substrate.compose_call( + call = subtensor.compose_call( call_module="Swap", call_function="toggle_user_liquidity", - call_params={"netuid": netuid, "enable": enable}, + call_params=LiquidityParams.toggle_user_liquidity( + netuid=netuid, + enable=enable, + ), ) return subtensor.sign_and_send_extrinsic( diff --git a/bittensor/core/extrinsics/move_stake.py b/bittensor/core/extrinsics/move_stake.py index 610c4c218a..d62a8c07ca 100644 --- a/bittensor/core/extrinsics/move_stake.py +++ b/bittensor/core/extrinsics/move_stake.py @@ -1,5 +1,6 @@ from typing import Optional, TYPE_CHECKING +from bittensor.core.extrinsics.params import MoveStakeParams from bittensor.core.types import ExtrinsicResponse from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging @@ -35,30 +36,32 @@ def _get_stake_in_origin_and_dest( return stake_in_origin, stake_in_destination -def transfer_stake_extrinsic( +def move_stake_extrinsic( subtensor: "Subtensor", wallet: "Wallet", - destination_coldkey_ss58: str, - hotkey_ss58: str, origin_netuid: int, + origin_hotkey_ss58: str, destination_netuid: int, - amount: Balance, + destination_hotkey_ss58: str, + amount: Optional[Balance] = None, + move_all_stake: bool = False, period: Optional[int] = None, raise_error: bool = False, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, ) -> ExtrinsicResponse: """ - Transfers stake from one subnet to another while changing the coldkey owner. + Moves stake to a different hotkey and/or subnet while keeping the same coldkey owner. Parameters: - subtensor: The subtensor instance to interact with the blockchain. - wallet: The wallet containing the coldkey to authorize the transfer. - destination_coldkey_ss58: SS58 address of the destination coldkey. - hotkey_ss58: SS58 address of the hotkey associated with the stake. - origin_netuid: Network UID of the origin subnet. - destination_netuid: Network UID of the destination subnet. - amount: The amount of stake to transfer as a `Balance` object. + subtensor: Subtensor instance. + wallet: The wallet to move stake from. + origin_netuid: The netuid of the source subnet. + origin_hotkey_ss58: The SS58 address of the source hotkey. + destination_netuid: The netuid of the destination subnet. + destination_hotkey_ss58: The SS58 address of the destination hotkey. + amount: Amount to move. + move_all_stake: If true, moves all stake from the source hotkey to the destination hotkey. 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. @@ -75,44 +78,50 @@ def transfer_stake_extrinsic( ).success: return unlocked - amount.set_unit(netuid=origin_netuid) + if not amount and not move_all_stake: + return ExtrinsicResponse( + False, + "Please specify an `amount` or `move_all_stake` argument to move stake.", + ).with_log() # Check sufficient stake stake_in_origin, stake_in_destination = _get_stake_in_origin_and_dest( subtensor=subtensor, - origin_hotkey_ss58=hotkey_ss58, - destination_hotkey_ss58=hotkey_ss58, origin_netuid=origin_netuid, + origin_hotkey_ss58=origin_hotkey_ss58, destination_netuid=destination_netuid, + destination_hotkey_ss58=destination_hotkey_ss58, origin_coldkey_ss58=wallet.coldkeypub.ss58_address, - destination_coldkey_ss58=destination_coldkey_ss58, + destination_coldkey_ss58=wallet.coldkeypub.ss58_address, ) - if stake_in_origin < amount: + if move_all_stake: + amount = stake_in_origin + + elif stake_in_origin < amount: return ExtrinsicResponse( False, - f"Insufficient stake in origin hotkey: {hotkey_ss58}. Stake: {stake_in_origin}, amount: {amount}.", + f"Insufficient stake in origin hotkey: {origin_hotkey_ss58}. Stake: {stake_in_origin}, amount: {amount}.", ).with_log() + amount.set_unit(netuid=origin_netuid) + logging.debug( - f"Transferring stake from coldkey [blue]{wallet.coldkeypub.ss58_address}[/blue] to coldkey " - f"[blue]{destination_coldkey_ss58}[/blue]" - ) - logging.debug( - f"Amount: [green]{amount}[/green] from netuid [yellow]{origin_netuid}[/yellow] to netuid " - f"[yellow]{destination_netuid}[/yellow]" + f"Moving stake from hotkey [blue]{origin_hotkey_ss58}[/blue] to hotkey [blue]{destination_hotkey_ss58}[/blue]\n" + f"Amount: [green]{amount}[/green] from netuid [yellow]{origin_netuid}[/yellow] to netuid [yellow]{destination_netuid}[/yellow]" ) - call = subtensor.substrate.compose_call( + call = subtensor.compose_call( call_module="SubtensorModule", - call_function="transfer_stake", - call_params={ - "destination_coldkey": destination_coldkey_ss58, - "hotkey": hotkey_ss58, - "origin_netuid": origin_netuid, - "destination_netuid": destination_netuid, - "alpha_amount": amount.rao, - }, + call_function="move_stake", + call_params=MoveStakeParams.move_stake( + origin_netuid=origin_netuid, + origin_hotkey_ss58=origin_hotkey_ss58, + destination_netuid=destination_netuid, + destination_hotkey_ss58=destination_hotkey_ss58, + amount=amount, + ), ) + block_before = subtensor.block response = subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, @@ -123,18 +132,29 @@ def transfer_stake_extrinsic( ) if response.success: + sim_swap = subtensor.sim_swap( + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + amount=amount, + block=block_before, + ) + response.transaction_tao_fee = sim_swap.tao_fee + response.transaction_alpha_fee = sim_swap.alpha_fee.set_unit(origin_netuid) + if not wait_for_finalization and not wait_for_inclusion: return response + logging.debug("[green]Finalized[/green]") + # Get updated stakes origin_stake, dest_stake = _get_stake_in_origin_and_dest( subtensor=subtensor, - origin_hotkey_ss58=hotkey_ss58, - destination_hotkey_ss58=hotkey_ss58, + origin_hotkey_ss58=origin_hotkey_ss58, + destination_hotkey_ss58=destination_hotkey_ss58, origin_netuid=origin_netuid, destination_netuid=destination_netuid, origin_coldkey_ss58=wallet.coldkeypub.ss58_address, - destination_coldkey_ss58=destination_coldkey_ss58, + destination_coldkey_ss58=wallet.coldkeypub.ss58_address, ) logging.debug( f"Origin Stake: [blue]{stake_in_origin}[/blue] :arrow_right: [green]{origin_stake}[/green]" @@ -143,6 +163,12 @@ def transfer_stake_extrinsic( f"Destination Stake: [blue]{stake_in_destination}[/blue] :arrow_right: [green]{dest_stake}[/green]" ) + response.data = { + "origin_stake_before": stake_in_origin, + "origin_stake_after": origin_stake, + "destination_stake_before": stake_in_destination, + "destination_stake_after": dest_stake, + } return response logging.error(f"[red]{response.message}[/red]") @@ -152,34 +178,30 @@ def transfer_stake_extrinsic( return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) -def swap_stake_extrinsic( +def transfer_stake_extrinsic( subtensor: "Subtensor", wallet: "Wallet", + destination_coldkey_ss58: str, hotkey_ss58: str, origin_netuid: int, destination_netuid: int, amount: Balance, - safe_swapping: bool = False, - allow_partial_stake: bool = False, - rate_tolerance: float = 0.005, period: Optional[int] = None, raise_error: bool = False, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, ) -> ExtrinsicResponse: """ - Moves stake between subnets while keeping the same coldkey-hotkey pair ownership. + Transfers stake from one subnet to another while changing the coldkey owner. Parameters: - subtensor: Subtensor instance. - wallet: The wallet to swap stake from. - hotkey_ss58: The hotkey SS58 address associated with the stake. - origin_netuid: The source subnet UID. - destination_netuid: The destination subnet UID. - amount: Amount to swap. - safe_swapping: If true, enables price safety checks to protect against price impact. - allow_partial_stake: If true, allows partial stake swaps when the full amount would exceed the price tolerance. - rate_tolerance: Maximum allowed increase in a price ratio (0.005 = 0.5%). + subtensor: The subtensor instance to interact with the blockchain. + wallet: The wallet containing the coldkey to authorize the transfer. + destination_coldkey_ss58: SS58 address of the destination coldkey. + hotkey_ss58: SS58 address of the hotkey associated with the stake. + origin_netuid: Network UID of the origin subnet. + destination_netuid: Network UID of the destination subnet. + amount: The amount of stake to transfer as a `Balance` object. 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. @@ -187,7 +209,6 @@ def swap_stake_extrinsic( wait_for_inclusion: Whether to wait for the inclusion of the transaction. wait_for_finalization: Whether to wait for the finalization of the transaction. - Returns: ExtrinsicResponse: The result object of the extrinsic execution. """ @@ -207,56 +228,35 @@ def swap_stake_extrinsic( origin_netuid=origin_netuid, destination_netuid=destination_netuid, origin_coldkey_ss58=wallet.coldkeypub.ss58_address, - destination_coldkey_ss58=wallet.coldkeypub.ss58_address, + destination_coldkey_ss58=destination_coldkey_ss58, ) - if stake_in_origin < amount: return ExtrinsicResponse( False, f"Insufficient stake in origin hotkey: {hotkey_ss58}. Stake: {stake_in_origin}, amount: {amount}.", ).with_log() - call_params = { - "hotkey": hotkey_ss58, - "origin_netuid": origin_netuid, - "destination_netuid": destination_netuid, - "alpha_amount": amount.rao, - } - - if safe_swapping: - 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.debug( - 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.debug( - 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( + logging.debug( + f"Transferring stake from coldkey [blue]{wallet.coldkeypub.ss58_address}[/blue] to coldkey " + f"[blue]{destination_coldkey_ss58}[/blue]" + ) + logging.debug( + f"Amount: [green]{amount}[/green] from netuid [yellow]{origin_netuid}[/yellow] to netuid " + f"[yellow]{destination_netuid}[/yellow]" + ) + call = subtensor.compose_call( call_module="SubtensorModule", - call_function=call_function, - call_params=call_params, + call_function="transfer_stake", + call_params=MoveStakeParams.transfer_stake( + hotkey_ss58=hotkey_ss58, + origin_netuid=origin_netuid, + destination_coldkey_ss58=destination_coldkey_ss58, + destination_netuid=destination_netuid, + amount=amount, + ), ) + block_before = subtensor.block response = subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, @@ -267,11 +267,18 @@ def swap_stake_extrinsic( ) if response.success: + sim_swap = subtensor.sim_swap( + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + amount=amount, + block=block_before, + ) + response.transaction_tao_fee = sim_swap.tao_fee + response.transaction_alpha_fee = sim_swap.alpha_fee.set_unit(origin_netuid) + if not wait_for_finalization and not wait_for_inclusion: return response - logging.debug("[green]Finalized[/green]") - # Get updated stakes origin_stake, dest_stake = _get_stake_in_origin_and_dest( subtensor=subtensor, @@ -280,9 +287,8 @@ def swap_stake_extrinsic( origin_netuid=origin_netuid, destination_netuid=destination_netuid, origin_coldkey_ss58=wallet.coldkeypub.ss58_address, - destination_coldkey_ss58=wallet.coldkeypub.ss58_address, + destination_coldkey_ss58=destination_coldkey_ss58, ) - logging.debug( f"Origin Stake: [blue]{stake_in_origin}[/blue] :arrow_right: [green]{origin_stake}[/green]" ) @@ -290,17 +296,8 @@ def swap_stake_extrinsic( f"Destination Stake: [blue]{stake_in_destination}[/blue] :arrow_right: [green]{dest_stake}[/green]" ) - response.data = { - "origin_stake_before": stake_in_origin, - "origin_stake_after": origin_stake, - "destination_stake_before": stake_in_destination, - "destination_stake_after": dest_stake, - } return response - if safe_swapping and "Custom error: 8" in response.message: - response.message = "Price ratio exceeded tolerance limit. Either increase price tolerance or enable partial staking." - logging.error(f"[red]{response.message}[/red]") return response @@ -308,32 +305,34 @@ def swap_stake_extrinsic( return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) -def move_stake_extrinsic( +def swap_stake_extrinsic( subtensor: "Subtensor", wallet: "Wallet", - origin_hotkey_ss58: str, + hotkey_ss58: str, origin_netuid: int, - destination_hotkey_ss58: str, destination_netuid: int, - amount: Optional[Balance] = None, - move_all_stake: bool = False, + amount: Balance, + safe_swapping: bool = False, + allow_partial_stake: bool = False, + rate_tolerance: float = 0.005, period: Optional[int] = None, raise_error: bool = False, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, ) -> ExtrinsicResponse: """ - Moves stake to a different hotkey and/or subnet while keeping the same coldkey owner. + Moves stake between subnets while keeping the same coldkey-hotkey pair ownership. Parameters: subtensor: Subtensor instance. - wallet: The wallet to move stake from. - origin_hotkey_ss58: The SS58 address of the source hotkey. - origin_netuid: The netuid of the source subnet. - destination_hotkey_ss58: The SS58 address of the destination hotkey. - destination_netuid: The netuid of the destination subnet. - amount: Amount to move. - move_all_stake: If true, moves all stake from the source hotkey to the destination hotkey. + wallet: The wallet to swap stake from. + hotkey_ss58: The hotkey SS58 address associated with the stake. + origin_netuid: The source subnet UID. + destination_netuid: The destination subnet UID. + amount: Amount to swap. + safe_swapping: If true, enables price safety checks to protect against price impact. + allow_partial_stake: If true, allows partial stake swaps when the full amount would exceed the price tolerance. + rate_tolerance: Maximum allowed increase in a price ratio (0.005 = 0.5%). 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. @@ -341,6 +340,7 @@ def move_stake_extrinsic( wait_for_inclusion: Whether to wait for the inclusion of the transaction. wait_for_finalization: Whether to wait for the finalization of the transaction. + Returns: ExtrinsicResponse: The result object of the extrinsic execution. """ @@ -350,49 +350,71 @@ def move_stake_extrinsic( ).success: return unlocked - if not amount and not move_all_stake: - return ExtrinsicResponse( - False, - "Please specify an `amount` or `move_all_stake` argument to move stake.", - ).with_log() + amount.set_unit(netuid=origin_netuid) # Check sufficient stake stake_in_origin, stake_in_destination = _get_stake_in_origin_and_dest( subtensor=subtensor, - origin_hotkey_ss58=origin_hotkey_ss58, - destination_hotkey_ss58=destination_hotkey_ss58, + origin_hotkey_ss58=hotkey_ss58, + destination_hotkey_ss58=hotkey_ss58, origin_netuid=origin_netuid, destination_netuid=destination_netuid, origin_coldkey_ss58=wallet.coldkeypub.ss58_address, destination_coldkey_ss58=wallet.coldkeypub.ss58_address, ) - if move_all_stake: - amount = stake_in_origin - elif stake_in_origin < amount: + if stake_in_origin < amount: return ExtrinsicResponse( False, - f"Insufficient stake in origin hotkey: {origin_hotkey_ss58}. Stake: {stake_in_origin}, amount: {amount}.", + f"Insufficient stake in origin hotkey: {hotkey_ss58}. Stake: {stake_in_origin}, amount: {amount}.", ).with_log() - amount.set_unit(netuid=origin_netuid) + if safe_swapping: + 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.debug( - f"Moving stake from hotkey [blue]{origin_hotkey_ss58}[/blue] to hotkey [blue]{destination_hotkey_ss58}[/blue]\n" - f"Amount: [green]{amount}[/green] from netuid [yellow]{origin_netuid}[/yellow] to netuid [yellow]{destination_netuid}[/yellow]" - ) - call = subtensor.substrate.compose_call( + call_function = "swap_stake_limit" + call_params = MoveStakeParams.swap_stake_limit( + hotkey_ss58=hotkey_ss58, + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + amount=amount, + allow_partial_stake=allow_partial_stake, + rate_tolerance=rate_tolerance, + origin_pool=origin_pool, + destination_pool=destination_pool, + ) + + logging.debug( + 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]" + ) + else: + call_function = "swap_stake" + call_params = MoveStakeParams.swap_stake( + hotkey_ss58=hotkey_ss58, + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + amount=amount, + ) + logging.debug( + 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 = subtensor.compose_call( call_module="SubtensorModule", - call_function="move_stake", - call_params={ - "origin_hotkey": origin_hotkey_ss58, - "origin_netuid": origin_netuid, - "destination_hotkey": destination_hotkey_ss58, - "destination_netuid": destination_netuid, - "alpha_amount": amount.rao, - }, + call_function=call_function, + call_params=call_params, ) + block_before = subtensor.block response = subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, @@ -403,6 +425,15 @@ def move_stake_extrinsic( ) if response.success: + sim_swap = subtensor.sim_swap( + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + amount=amount, + block=block_before, + ) + response.transaction_tao_fee = sim_swap.tao_fee + response.transaction_alpha_fee = sim_swap.alpha_fee.set_unit(origin_netuid) + if not wait_for_finalization and not wait_for_inclusion: return response @@ -411,13 +442,14 @@ def move_stake_extrinsic( # Get updated stakes origin_stake, dest_stake = _get_stake_in_origin_and_dest( subtensor=subtensor, - origin_hotkey_ss58=origin_hotkey_ss58, - destination_hotkey_ss58=destination_hotkey_ss58, + origin_hotkey_ss58=hotkey_ss58, + destination_hotkey_ss58=hotkey_ss58, origin_netuid=origin_netuid, destination_netuid=destination_netuid, origin_coldkey_ss58=wallet.coldkeypub.ss58_address, destination_coldkey_ss58=wallet.coldkeypub.ss58_address, ) + logging.debug( f"Origin Stake: [blue]{stake_in_origin}[/blue] :arrow_right: [green]{origin_stake}[/green]" ) @@ -433,6 +465,9 @@ def move_stake_extrinsic( } return response + if safe_swapping and "Custom error: 8" in response.message: + response.message = "Price ratio exceeded tolerance limit. Either increase price tolerance or enable partial staking." + logging.error(f"[red]{response.message}[/red]") return response diff --git a/bittensor/core/extrinsics/params/__init__.py b/bittensor/core/extrinsics/params/__init__.py new file mode 100644 index 0000000000..516dcf1f63 --- /dev/null +++ b/bittensor/core/extrinsics/params/__init__.py @@ -0,0 +1,29 @@ +from .children import ChildrenParams +from .liquidity import LiquidityParams +from .move_stake import MoveStakeParams +from .registration import RegistrationParams +from .root import RootParams +from .serving import ServingParams +from .staking import StakingParams +from .start_call import StartCallParams +from .take import TakeParams +from .transfer import TransferParams, get_transfer_fn_params +from .unstaking import UnstakingParams +from .weights import WeightsParams + + +__all__ = [ + "get_transfer_fn_params", + "ChildrenParams", + "LiquidityParams", + "MoveStakeParams", + "RegistrationParams", + "RootParams", + "ServingParams", + "StakingParams", + "StartCallParams", + "TakeParams", + "TransferParams", + "UnstakingParams", + "WeightsParams", +] diff --git a/bittensor/core/extrinsics/params/children.py b/bittensor/core/extrinsics/params/children.py new file mode 100644 index 0000000000..eeefa44371 --- /dev/null +++ b/bittensor/core/extrinsics/params/children.py @@ -0,0 +1,32 @@ +from dataclasses import dataclass + +from bittensor.utils import float_to_u64 + + +@dataclass +class ChildrenParams: + @classmethod + def set_children( + cls, + hotkey_ss58: str, + netuid: int, + children: list[tuple[float, str]], + ) -> dict: + """Returns the parameters for the `set_children`.""" + params = { + "children": [ + (float_to_u64(proportion), child_hotkey) + for proportion, child_hotkey in children + ], + "hotkey": hotkey_ss58, + "netuid": netuid, + } + return params + + @classmethod + def set_pending_childkey_cooldown( + cls, + cooldown: int, + ) -> dict: + """Returns the parameters for the `set_pending_childkey_cooldown`.""" + return {"cooldown": cooldown} diff --git a/bittensor/core/extrinsics/params/liquidity.py b/bittensor/core/extrinsics/params/liquidity.py new file mode 100644 index 0000000000..be37883a00 --- /dev/null +++ b/bittensor/core/extrinsics/params/liquidity.py @@ -0,0 +1,67 @@ +from dataclasses import dataclass + +from bittensor.utils.balance import Balance +from bittensor.utils.liquidity import price_to_tick + + +@dataclass +class LiquidityParams: + @classmethod + def add_liquidity( + cls, + netuid: int, + hotkey_ss58: str, + liquidity: Balance, + price_low: Balance, + price_high: Balance, + ) -> dict: + """Returns the parameters for the `add_liquidity`.""" + tick_low = price_to_tick(price_low.tao) + tick_high = price_to_tick(price_high.tao) + + return { + "hotkey": hotkey_ss58, + "netuid": netuid, + "tick_low": tick_low, + "tick_high": tick_high, + "liquidity": liquidity.rao, + } + + @classmethod + def modify_position( + cls, + netuid: int, + hotkey_ss58: str, + position_id: int, + liquidity_delta: Balance, + ) -> dict: + """Returns the parameters for the `modify_position`.""" + return { + "hotkey": hotkey_ss58, + "netuid": netuid, + "position_id": position_id, + "liquidity_delta": liquidity_delta.rao, + } + + @classmethod + def remove_liquidity( + cls, + netuid: int, + hotkey_ss58: str, + position_id: int, + ) -> dict: + """Returns the parameters for the `remove_liquidity`.""" + return { + "hotkey": hotkey_ss58, + "netuid": netuid, + "position_id": position_id, + } + + @classmethod + def toggle_user_liquidity( + cls, + netuid: int, + enable: bool, + ) -> dict: + """Returns the parameters for the `toggle_user_liquidity`.""" + return {"netuid": netuid, "enable": enable} diff --git a/bittensor/core/extrinsics/params/move_stake.py b/bittensor/core/extrinsics/params/move_stake.py new file mode 100644 index 0000000000..7ab9d94d5f --- /dev/null +++ b/bittensor/core/extrinsics/params/move_stake.py @@ -0,0 +1,90 @@ +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from bittensor.utils.balance import Balance + +if TYPE_CHECKING: + from bittensor.core.chain_data import DynamicInfo + + +@dataclass +class MoveStakeParams: + @classmethod + def move_stake( + cls, + origin_netuid: int, + origin_hotkey_ss58: str, + destination_netuid: int, + destination_hotkey_ss58: str, + amount: Balance, + ) -> dict: + """Returns the parameters for the `move_stake`.""" + return { + "origin_netuid": origin_netuid, + "origin_hotkey": origin_hotkey_ss58, + "destination_netuid": destination_netuid, + "destination_hotkey": destination_hotkey_ss58, + "alpha_amount": amount.rao, + } + + @classmethod + def transfer_stake( + cls, + destination_coldkey_ss58: str, + hotkey_ss58: str, + origin_netuid: int, + destination_netuid: int, + amount: Balance, + ) -> dict: + """Returns the parameters for the `transfer_stake`.""" + return { + "destination_coldkey": destination_coldkey_ss58, + "hotkey": hotkey_ss58, + "origin_netuid": origin_netuid, + "destination_netuid": destination_netuid, + "alpha_amount": amount.rao, + } + + @classmethod + def swap_stake( + cls, + hotkey_ss58: str, + origin_netuid: int, + destination_netuid: int, + amount: Balance, + ) -> dict: + """Returns the parameters for the `swap_stake`.""" + return { + "hotkey": hotkey_ss58, + "origin_netuid": origin_netuid, + "destination_netuid": destination_netuid, + "alpha_amount": amount.rao, + } + + @classmethod + def swap_stake_limit( + cls, + hotkey_ss58: str, + origin_netuid: int, + destination_netuid: int, + amount: Balance, + allow_partial_stake: bool, + rate_tolerance: float, + origin_pool: "DynamicInfo", + destination_pool: "DynamicInfo", + ) -> dict: + """Returns the parameters for the `swap_stake_limit`.""" + call_params = cls.swap_stake( + hotkey_ss58, origin_netuid, destination_netuid, amount + ) + + swap_rate_ratio = origin_pool.price.rao / destination_pool.price.rao + swap_rate_ratio_with_tolerance = swap_rate_ratio * (1 + rate_tolerance) + + call_params.update( + { + "limit_price": swap_rate_ratio_with_tolerance, + "allow_partial": allow_partial_stake, + } + ) + return call_params diff --git a/bittensor/core/extrinsics/params/registration.py b/bittensor/core/extrinsics/params/registration.py new file mode 100644 index 0000000000..816db7a186 --- /dev/null +++ b/bittensor/core/extrinsics/params/registration.py @@ -0,0 +1,72 @@ +from dataclasses import dataclass + + +@dataclass +class RegistrationParams: + @classmethod + def burned_register( + cls, + netuid: int, + hotkey_ss58: str, + ) -> dict: + """Returns the parameters for the `burned_register`.""" + return { + "netuid": netuid, + "hotkey": hotkey_ss58, + } + + @classmethod + def register_network( + cls, + hotkey_ss58: str, + ) -> dict: + """Returns the parameters for the `register_network`.""" + return {"hotkey": hotkey_ss58} + + @classmethod + def register( + cls, + netuid: int, + coldkey_ss58: str, + hotkey_ss58: str, + block_number: int, + nonce: int, + work: list[int], + ) -> dict: + """Returns the parameters for the `register`.""" + return { + "coldkey": coldkey_ss58, + "hotkey": hotkey_ss58, + "netuid": netuid, + "block_number": block_number, + "nonce": nonce, + "work": work, + } + + @classmethod + def set_subnet_identity( + cls, + netuid: int, + hotkey_ss58: str, + subnet_name: str, + github_repo: str, + subnet_contact: str, + subnet_url: str, + logo_url: str, + discord: str, + description: str, + additional: str, + ) -> dict: + """Returns the parameters for the `set_subnet_identity`.""" + return { + "hotkey": hotkey_ss58, + "netuid": netuid, + "subnet_name": subnet_name, + "github_repo": github_repo, + "subnet_contact": subnet_contact, + "subnet_url": subnet_url, + "logo_url": logo_url, + "discord": discord, + "description": description, + "additional": additional, + } diff --git a/bittensor/core/extrinsics/params/root.py b/bittensor/core/extrinsics/params/root.py new file mode 100644 index 0000000000..add3cc8c50 --- /dev/null +++ b/bittensor/core/extrinsics/params/root.py @@ -0,0 +1,12 @@ +from dataclasses import dataclass + + +@dataclass +class RootParams: + @classmethod + def root_register( + cls, + hotkey_ss58: str, + ) -> dict: + """Returns the parameters for the `root_register`.""" + return {"hotkey": hotkey_ss58} diff --git a/bittensor/core/extrinsics/params/serving.py b/bittensor/core/extrinsics/params/serving.py new file mode 100644 index 0000000000..5bfaf54aa8 --- /dev/null +++ b/bittensor/core/extrinsics/params/serving.py @@ -0,0 +1,43 @@ +from dataclasses import dataclass +from typing import Optional +from bittensor.utils import Certificate, networking as net +from bittensor.core.types import AxonServeCallParams +from bittensor.core.settings import version_as_int + + +@dataclass +class ServingParams: + @classmethod + def serve_axon_and_tls( + cls, + hotkey_ss58: str, + coldkey_ss58: str, + netuid: int, + ip: str, + port: int, + protocol: int, + placeholder1: int, + placeholder2: int, + certificate: Optional[Certificate] = None, + ) -> AxonServeCallParams: + """Returns the parameters for the `root_register`.""" + return AxonServeCallParams( + **{ + "hotkey": hotkey_ss58, + "coldkey": coldkey_ss58, + "netuid": netuid, + "ip": net.ip_to_int(ip), + "port": port, + "protocol": protocol, + "certificate": certificate, + "ip_type": net.ip_version(ip), + "version": version_as_int, + "placeholder1": placeholder1, + "placeholder2": placeholder2, + } + ) + + @classmethod + def set_commitment(cls, netuid: int, info_fields: list) -> dict: + """Returns the parameters for the `set_commitment`.""" + return {"netuid": netuid, "info": {"fields": [info_fields]}} diff --git a/bittensor/core/extrinsics/params/staking.py b/bittensor/core/extrinsics/params/staking.py new file mode 100644 index 0000000000..a4bcb33a63 --- /dev/null +++ b/bittensor/core/extrinsics/params/staking.py @@ -0,0 +1,57 @@ +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from bittensor.utils.balance import Balance + +if TYPE_CHECKING: + from bittensor.core.chain_data import DynamicInfo + + +@dataclass +class StakingParams: + @classmethod + def add_stake( + cls, + netuid: int, + hotkey_ss58: str, + amount: Balance, + ) -> dict: + """Returns the parameters for the `safe` parameters.""" + return { + "netuid": netuid, + "hotkey": hotkey_ss58, + "amount_staked": amount.rao, + } + + @classmethod + def add_stake_limit( + cls, + netuid: int, + hotkey_ss58: str, + amount: "Balance", + allow_partial_stake: bool, + rate_tolerance: float, + pool: "DynamicInfo", + ) -> dict: + """Returns the parameters for the `add_stake_limit`.""" + call_params = cls.add_stake(netuid, hotkey_ss58, amount) + + base_price = pool.price.tao + price_with_tolerance = ( + base_price if pool.netuid == 0 else base_price * (1 + rate_tolerance) + ) + limit_price = Balance.from_tao(price_with_tolerance).rao + + call_params.update( + {"limit_price": limit_price, "allow_partial": allow_partial_stake} + ) + return call_params + + @classmethod + def set_coldkey_auto_stake_hotkey( + cls, + netuid: int, + hotkey_ss58: str, + ) -> dict: + """Returns the parameters for the `set_auto_stake_extrinsic`.""" + return {"hotkey": hotkey_ss58, "netuid": netuid} diff --git a/bittensor/core/extrinsics/params/start_call.py b/bittensor/core/extrinsics/params/start_call.py new file mode 100644 index 0000000000..cab30bdeea --- /dev/null +++ b/bittensor/core/extrinsics/params/start_call.py @@ -0,0 +1,12 @@ +from dataclasses import dataclass + + +@dataclass +class StartCallParams: + @classmethod + def start_call( + cls, + netuid: int, + ) -> dict: + """Returns the parameters for the `start_call`.""" + return {"netuid": netuid} diff --git a/bittensor/core/extrinsics/params/take.py b/bittensor/core/extrinsics/params/take.py new file mode 100644 index 0000000000..bc11b8b93c --- /dev/null +++ b/bittensor/core/extrinsics/params/take.py @@ -0,0 +1,13 @@ +from dataclasses import dataclass + + +@dataclass +class TakeParams: + @classmethod + def increase_decrease_take( + cls, + hotkey_ss58: str, + take: int, + ) -> dict: + """Returns the parameters for the `increase_take` and `decrease_take`.""" + return {"hotkey": hotkey_ss58, "take": take} diff --git a/bittensor/core/extrinsics/params/transfer.py b/bittensor/core/extrinsics/params/transfer.py new file mode 100644 index 0000000000..b0240f1bb2 --- /dev/null +++ b/bittensor/core/extrinsics/params/transfer.py @@ -0,0 +1,71 @@ +from dataclasses import dataclass +from typing import Optional, Union + +from bittensor.utils.balance import Balance + + +def get_transfer_fn_params( + amount: Optional["Balance"], destination: str, keep_alive: bool +) -> tuple[str, dict[str, Union[str, int, bool]]]: + """ + Helper function to get the transfer call function and call params, depending on the value and keep_alive flag + provided. + + Parameters: + amount: the amount of Tao to transfer. `None` if transferring all. + destination: the destination SS58 of the transfer + keep_alive: whether to enforce a retention of the existential deposit in the account after transfer. + + Returns: + tuple[call function, call params] + """ + call_params: dict[str, Union[str, int, bool]] = {"dest": destination} + if amount is None: + call_function = "transfer_all" + if keep_alive: + call_params["keep_alive"] = True + else: + call_params["keep_alive"] = False + else: + call_params["value"] = amount.rao + if keep_alive: + call_function = "transfer_keep_alive" + else: + call_function = "transfer_allow_death" + return call_function, call_params + + +@dataclass +class TransferParams: + @classmethod + def transfer_all( + cls, + destination: str, + amount: Optional[Balance] = None, + keep_alive: bool = True, + ) -> dict: + """Returns the parameters for the `transfer_all`.""" + _, call_params = get_transfer_fn_params(amount, destination, keep_alive) + return call_params + + @classmethod + def transfer_keep_alive( + cls, + destination: str, + amount: Optional[Balance] = None, + keep_alive: bool = True, + ) -> dict: + """Returns the parameters for the `transfer_keep_alive`.""" + _, call_params = get_transfer_fn_params(amount, destination, keep_alive) + return call_params + + @classmethod + def transfer_allow_death( + cls, + destination: str, + amount: Optional[Balance] = None, + keep_alive: bool = True, + ) -> dict: + """Returns the parameters for the `transfer_allow_death`.""" + _, call_params = get_transfer_fn_params(amount, destination, keep_alive) + return call_params diff --git a/bittensor/core/extrinsics/params/unstaking.py b/bittensor/core/extrinsics/params/unstaking.py new file mode 100644 index 0000000000..e959cc63ec --- /dev/null +++ b/bittensor/core/extrinsics/params/unstaking.py @@ -0,0 +1,63 @@ +from dataclasses import dataclass +from typing import Optional + +from bittensor.core.chain_data import DynamicInfo +from bittensor.utils.balance import Balance + + +@dataclass +class UnstakingParams: + @classmethod + def remove_stake(cls, netuid: int, hotkey_ss58: str, amount: "Balance") -> dict: + """Returns the parameters for the `remove_stake`.""" + return {"netuid": netuid, "hotkey": hotkey_ss58, "amount_unstaked": amount.rao} + + @classmethod + def remove_stake_limit( + cls, + netuid: int, + hotkey_ss58: str, + amount: "Balance", + allow_partial_stake: bool, + rate_tolerance: float, + pool: "DynamicInfo", + ) -> dict: + """Returns the parameters for the `remove_stake_limit`.""" + call_params = cls.remove_stake(netuid, hotkey_ss58, amount) + + base_price = pool.price.tao + + if pool.netuid == 0: + price_with_tolerance = base_price + else: + price_with_tolerance = base_price * (1 - rate_tolerance) + + limit_price = Balance.from_tao(price_with_tolerance).rao + call_params.update( + { + "limit_price": limit_price, + "allow_partial": allow_partial_stake, + } + ) + return call_params + + @classmethod + def remove_stake_full_limit( + cls, + netuid: int, + hotkey_ss58: str, + rate_tolerance: Optional[float] = None, + pool: Optional["DynamicInfo"] = None, + ) -> dict: + """Returns the parameters for the `remove_stake_full_limit`.""" + call_params = { + "hotkey": hotkey_ss58, + "netuid": netuid, + "limit_price": None, + } + + if rate_tolerance: + limit_price = pool.price * (1 - rate_tolerance) + call_params.update({"limit_price": limit_price}) + + return call_params diff --git a/bittensor/core/extrinsics/params/weights.py b/bittensor/core/extrinsics/params/weights.py new file mode 100644 index 0000000000..c56a8df8f6 --- /dev/null +++ b/bittensor/core/extrinsics/params/weights.py @@ -0,0 +1,76 @@ +from dataclasses import dataclass + +from bittensor.core.types import UIDs, Weights, Salt + + +@dataclass +class WeightsParams: + @classmethod + def commit_timelocked_mechanism_weights( + cls, + netuid: int, + mechid: int, + commit_for_reveal: bytes, + reveal_round: int, + commit_reveal_version: int, + ) -> dict: + """Returns the parameters for the `commit_timelocked_mechanism_weights`.""" + return { + "netuid": netuid, + "mecid": mechid, + "commit": commit_for_reveal, + "reveal_round": reveal_round, + "commit_reveal_version": commit_reveal_version, + } + + @classmethod + def commit_mechanism_weights( + cls, + netuid: int, + mechid: int, + commit_hash: str, + ) -> dict: + """Returns the parameters for the `commit_mechanism_weights`.""" + return { + "netuid": netuid, + "mecid": mechid, + "commit_hash": commit_hash, + } + + @classmethod + def reveal_mechanism_weights( + cls, + netuid: int, + mechid: int, + uids: UIDs, + weights: Weights, + salt: Salt, + version_key: int, + ) -> dict: + """Returns the parameters for the `reveal_mechanism_weights`.""" + return { + "netuid": netuid, + "mecid": mechid, + "uids": uids, + "values": weights, + "salt": salt, + "version_key": version_key, + } + + @classmethod + def set_mechanism_weights( + cls, + netuid: int, + mechid: int, + uids: UIDs, + weights: Weights, + version_key: int, + ) -> dict: + """Returns the parameters for the `set_mechanism_weights`.""" + return { + "netuid": netuid, + "mecid": mechid, + "dests": uids, + "weights": weights, + "version_key": version_key, + } diff --git a/bittensor/core/extrinsics/registration.py b/bittensor/core/extrinsics/registration.py index 351af9ecc1..95abed4bd2 100644 --- a/bittensor/core/extrinsics/registration.py +++ b/bittensor/core/extrinsics/registration.py @@ -6,6 +6,7 @@ from typing import Optional, Union, TYPE_CHECKING from bittensor.core.errors import RegistrationError +from bittensor.core.extrinsics.params import RegistrationParams from bittensor.core.types import ExtrinsicResponse from bittensor.utils.btlogging import logging from bittensor.utils.registration import create_pow, log_no_torch_error, torch @@ -42,21 +43,25 @@ def burned_register_extrinsic( """ try: if not ( - unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + unlocked := ExtrinsicResponse.unlock_wallet( + wallet, raise_error, unlock_type="both" + ) ).success: return unlocked block = subtensor.get_current_block() - if not subtensor.subnet_exists(netuid, block=block): + if not subtensor.subnet_exists(netuid=netuid, block=block): return ExtrinsicResponse( False, f"Subnet {netuid} does not exist." ).with_log() neuron = subtensor.get_neuron_for_pubkey_and_subnet( - wallet.hotkey.ss58_address, netuid=netuid, block=block + netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address, block=block ) - old_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address, block=block) + old_balance = subtensor.get_balance( + address=wallet.coldkeypub.ss58_address, block=block + ) if not neuron.is_null: message = "Already registered." @@ -72,13 +77,13 @@ def burned_register_extrinsic( recycle_amount = subtensor.recycle(netuid=netuid, block=block) logging.debug(f"Recycling {recycle_amount} to register on subnet:{netuid}") - call = subtensor.substrate.compose_call( + call = subtensor.compose_call( call_module="SubtensorModule", call_function="burned_register", - call_params={ - "netuid": netuid, - "hotkey": wallet.hotkey.ss58_address, - }, + call_params=RegistrationParams.burned_register( + netuid=netuid, + hotkey_ss58=wallet.hotkey.ss58_address, + ), ) response = subtensor.sign_and_send_extrinsic( call=call, @@ -98,7 +103,7 @@ def burned_register_extrinsic( return response # Successful registration, final check for neuron and pubkey - new_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) + new_balance = subtensor.get_balance(address=wallet.coldkeypub.ss58_address) logging.debug( f"Balance: [blue]{old_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" @@ -157,7 +162,9 @@ def register_subnet_extrinsic( """ try: if not ( - unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + unlocked := ExtrinsicResponse.unlock_wallet( + wallet, raise_error, unlock_type="both" + ) ).success: return unlocked @@ -170,12 +177,12 @@ def register_subnet_extrinsic( f"Insufficient balance {balance} to register subnet. Current burn cost is {burn_cost} TAO.", ).with_log() - call = subtensor.substrate.compose_call( + call = subtensor.compose_call( call_module="SubtensorModule", call_function="register_network", - call_params={ - "hotkey": wallet.hotkey.ss58_address, - }, + call_params=RegistrationParams.register_network( + hotkey_ss58=wallet.hotkey.ss58_address + ), ) response = subtensor.sign_and_send_extrinsic( @@ -244,7 +251,9 @@ def register_extrinsic( """ try: if not ( - unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + unlocked := ExtrinsicResponse.unlock_wallet( + wallet, raise_error, unlock_type="both" + ) ).success: return unlocked @@ -327,17 +336,17 @@ def register_extrinsic( # check if a pow result is still valid while not pow_result.is_stale(subtensor=subtensor): # create extrinsic call - call = subtensor.substrate.compose_call( + call = subtensor.compose_call( call_module="SubtensorModule", call_function="register", - call_params={ - "netuid": netuid, - "block_number": pow_result.block_number, - "nonce": pow_result.nonce, - "work": [int(byte_) for byte_ in pow_result.seal], - "hotkey": wallet.hotkey.ss58_address, - "coldkey": wallet.coldkeypub.ss58_address, - }, + call_params=RegistrationParams.register( + netuid=netuid, + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=wallet.hotkey.ss58_address, + block_number=pow_result.block_number, + nonce=pow_result.nonce, + work=[int(byte_) for byte_ in pow_result.seal], + ), ) response = subtensor.sign_and_send_extrinsic( call=call, @@ -432,25 +441,27 @@ def set_subnet_identity_extrinsic( """ try: if not ( - unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + unlocked := ExtrinsicResponse.unlock_wallet( + wallet, raise_error, unlock_type="both" + ) ).success: return unlocked - call = subtensor.substrate.compose_call( + call = subtensor.compose_call( call_module="SubtensorModule", call_function="set_subnet_identity", - call_params={ - "hotkey": wallet.hotkey.ss58_address, - "netuid": netuid, - "subnet_name": subnet_name, - "github_repo": github_repo, - "subnet_contact": subnet_contact, - "subnet_url": subnet_url, - "logo_url": logo_url, - "discord": discord, - "description": description, - "additional": additional, - }, + call_params=RegistrationParams.set_subnet_identity( + hotkey_ss58=wallet.hotkey.ss58_address, + netuid=netuid, + subnet_name=subnet_name, + github_repo=github_repo, + subnet_contact=subnet_contact, + subnet_url=subnet_url, + logo_url=logo_url, + discord=discord, + description=description, + additional=additional, + ), ) response = subtensor.sign_and_send_extrinsic( diff --git a/bittensor/core/extrinsics/root.py b/bittensor/core/extrinsics/root.py index 91f3753c16..b924a60018 100644 --- a/bittensor/core/extrinsics/root.py +++ b/bittensor/core/extrinsics/root.py @@ -1,6 +1,7 @@ import time from typing import Optional, TYPE_CHECKING +from bittensor.core.extrinsics.params import RootParams from bittensor.core.types import ExtrinsicResponse from bittensor.utils import u16_normalized_float from bittensor.utils.balance import Balance @@ -59,7 +60,9 @@ def root_register_extrinsic( """ try: if not ( - unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + unlocked := ExtrinsicResponse.unlock_wallet( + wallet, raise_error, unlock_type="both" + ) ).success: return unlocked @@ -96,10 +99,10 @@ def root_register_extrinsic( if is_registered: return ExtrinsicResponse(message="Already registered on root network.") - call = subtensor.substrate.compose_call( + call = subtensor.compose_call( call_module="SubtensorModule", call_function="root_register", - call_params={"hotkey": wallet.hotkey.ss58_address}, + call_params=RootParams.root_register(wallet.hotkey.ss58_address), ) response = subtensor.sign_and_send_extrinsic( call=call, diff --git a/bittensor/core/extrinsics/serving.py b/bittensor/core/extrinsics/serving.py index a9b1184dee..56b697813e 100644 --- a/bittensor/core/extrinsics/serving.py +++ b/bittensor/core/extrinsics/serving.py @@ -1,8 +1,8 @@ from typing import Optional, Union, TYPE_CHECKING from bittensor.core.errors import MetadataError -from bittensor.core.settings import version_as_int -from bittensor.core.types import AxonServeCallParams, ExtrinsicResponse +from bittensor.core.extrinsics.params.serving import ServingParams +from bittensor.core.types import ExtrinsicResponse from bittensor.utils import ( networking as net, Certificate, @@ -54,29 +54,24 @@ def serve_extrinsic( ExtrinsicResponse: The result object of the extrinsic execution. """ try: - signing_keypair = "hotkey" if not ( unlocked := ExtrinsicResponse.unlock_wallet( - wallet, raise_error, signing_keypair + wallet, raise_error, unlock_type="both" ) ).success: return unlocked - - params = AxonServeCallParams( - **{ - "version": version_as_int, - "ip": net.ip_to_int(ip), - "port": port, - "ip_type": net.ip_version(ip), - "netuid": netuid, - "hotkey": wallet.hotkey.ss58_address, - "coldkey": wallet.coldkeypub.ss58_address, - "protocol": protocol, - "placeholder1": placeholder1, - "placeholder2": placeholder2, - "certificate": certificate, - } + params = ServingParams.serve_axon_and_tls( + hotkey_ss58=wallet.hotkey.ss58_address, + coldkey_ss58=wallet.coldkeypub.ss58_address, + netuid=netuid, + ip=ip, + port=port, + protocol=protocol, + placeholder1=placeholder1, + placeholder2=placeholder2, + certificate=certificate, ) + logging.debug("Checking axon ...") neuron = subtensor.get_neuron_for_pubkey_and_subnet( wallet.hotkey.ss58_address, netuid=netuid @@ -97,7 +92,7 @@ def serve_extrinsic( else: call_function = "serve_axon_tls" - call = subtensor.substrate.compose_call( + call = subtensor.compose_call( call_module="SubtensorModule", call_function=call_function, call_params=params.dict(), @@ -108,7 +103,7 @@ def serve_extrinsic( wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, - sign_with=signing_keypair, + sign_with="hotkey", period=period, raise_error=raise_error, ) @@ -156,13 +151,6 @@ def serve_axon_extrinsic( ExtrinsicResponse: The result object of the extrinsic execution. """ try: - if not ( - unlocked := ExtrinsicResponse.unlock_wallet( - axon.wallet, raise_error, "hotkey" - ) - ).success: - return unlocked - external_port = axon.external_port # ---- Get external ip ---- @@ -257,19 +245,16 @@ def publish_metadata_extrinsic( if reset_bonds: fields.append({"ResetBondsFlag": b""}) - call = subtensor.substrate.compose_call( + call = subtensor.compose_call( call_module="Commitments", call_function="set_commitment", - call_params={ - "netuid": netuid, - "info": {"fields": [fields]}, - }, + call_params=ServingParams.set_commitment(netuid=netuid, info_fields=fields), ) response = subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, - sign_with="hotkey", + sign_with=signing_keypair, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, period=period, diff --git a/bittensor/core/extrinsics/staking.py b/bittensor/core/extrinsics/staking.py index 79e26303ee..f9cf0a6fa9 100644 --- a/bittensor/core/extrinsics/staking.py +++ b/bittensor/core/extrinsics/staking.py @@ -3,6 +3,7 @@ from async_substrate_interface.errors import SubstrateRequestException from bittensor.core.errors import BalanceTypeError +from bittensor.core.extrinsics.params import StakingParams from bittensor.core.extrinsics.utils import get_old_stakes from bittensor.core.types import ExtrinsicResponse, UIDs from bittensor.utils import format_error_message @@ -90,50 +91,45 @@ def add_stake_extrinsic( logging.debug(f"\t\twallet: {wallet.name}") return ExtrinsicResponse(False, f"{message}.").with_log() - call_params = { - "hotkey": hotkey_ss58, - "netuid": netuid, - "amount_staked": amount.rao, - } - if safe_staking: pool = subtensor.subnet(netuid=netuid) - base_price = pool.price.tao - price_with_tolerance = ( - base_price if pool.netuid == 0 else base_price * (1 + rate_tolerance) + call_function = "add_stake_limit" + call_params = StakingParams.add_stake_limit( + netuid=netuid, + hotkey_ss58=hotkey_ss58, + amount=amount, + allow_partial_stake=allow_partial_stake, + rate_tolerance=rate_tolerance, + pool=pool, ) logging.debug( f"Safe Staking to: [blue]netuid: [green]{netuid}[/green], amount: [green]{amount}[/green], " f"tolerance percentage: [green]{rate_tolerance * 100}%[/green], " - f"price limit: [green]{price_with_tolerance}[/green], " - f"original price: [green]{base_price}[/green], " + f"price limit: [green]{call_params.get('limit_price')}[/green], " + f"original price: [green]{pool.price}[/green], " f"with partial stake: [green]{allow_partial_stake}[/green] " f"on [blue]{subtensor.network}[/blue]." ) - limit_price = Balance.from_tao(price_with_tolerance).rao - call_params.update( - { - "limit_price": limit_price, - "allow_partial": allow_partial_stake, - } - ) - call_function = "add_stake_limit" else: + call_function = "add_stake" + call_params = StakingParams.add_stake( + netuid=netuid, hotkey_ss58=hotkey_ss58, amount=amount + ) logging.debug( f"Staking to: [blue]netuid: [green]{netuid}[/green], amount: [green]{amount}[/green] " f"on [blue]{subtensor.network}[/blue]." ) - call_function = "add_stake" - call = subtensor.substrate.compose_call( + call = subtensor.compose_call( call_module="SubtensorModule", call_function=call_function, call_params=call_params, ) + block_before = subtensor.block response = subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, @@ -145,6 +141,15 @@ def add_stake_extrinsic( 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]") @@ -436,13 +441,12 @@ def set_auto_stake_extrinsic( ).success: return unlocked - call = subtensor.substrate.compose_call( + call = subtensor.compose_call( call_module="SubtensorModule", call_function="set_coldkey_auto_stake_hotkey", - call_params={ - "netuid": netuid, - "hotkey": hotkey_ss58, - }, + call_params=StakingParams.set_coldkey_auto_stake_hotkey( + netuid, hotkey_ss58 + ), ) response = subtensor.sign_and_send_extrinsic( call=call, diff --git a/bittensor/core/extrinsics/start_call.py b/bittensor/core/extrinsics/start_call.py index af4de4620c..9cc1131ad6 100644 --- a/bittensor/core/extrinsics/start_call.py +++ b/bittensor/core/extrinsics/start_call.py @@ -1,5 +1,6 @@ from typing import TYPE_CHECKING, Optional +from bittensor.core.extrinsics.params import StartCallParams from bittensor.core.types import ExtrinsicResponse if TYPE_CHECKING: @@ -40,10 +41,10 @@ def start_call_extrinsic( ).success: return unlocked - start_call = subtensor.substrate.compose_call( + start_call = subtensor.compose_call( call_module="SubtensorModule", call_function="start_call", - call_params={"netuid": netuid}, + call_params=StartCallParams.start_call(netuid=netuid), ) return subtensor.sign_and_send_extrinsic( diff --git a/bittensor/core/extrinsics/take.py b/bittensor/core/extrinsics/take.py index 325a50ee9c..4a0c38cf83 100644 --- a/bittensor/core/extrinsics/take.py +++ b/bittensor/core/extrinsics/take.py @@ -2,6 +2,7 @@ from bittensor_wallet.bittensor_wallet import Wallet +from bittensor.core.extrinsics.params import TakeParams from bittensor.core.types import ExtrinsicResponse if TYPE_CHECKING: @@ -43,13 +44,10 @@ def set_take_extrinsic( ).success: return unlocked - call = subtensor.substrate.compose_call( + call = subtensor.compose_call( call_module="SubtensorModule", call_function=action, - call_params={ - "hotkey": hotkey_ss58, - "take": take, - }, + call_params=TakeParams.increase_decrease_take(hotkey_ss58, take), ) return subtensor.sign_and_send_extrinsic( call=call, diff --git a/bittensor/core/extrinsics/transfer.py b/bittensor/core/extrinsics/transfer.py index 56bd44f0e8..1667e79ad5 100644 --- a/bittensor/core/extrinsics/transfer.py +++ b/bittensor/core/extrinsics/transfer.py @@ -1,10 +1,10 @@ from typing import TYPE_CHECKING, Optional +from bittensor.core.extrinsics.params import get_transfer_fn_params from bittensor.core.settings import NETWORK_EXPLORER_MAP, DEFAULT_NETWORK from bittensor.core.types import ExtrinsicResponse from bittensor.utils import ( get_explorer_url_for_network, - get_transfer_fn_params, is_valid_bittensor_address_or_public_key, ) from bittensor.utils.balance import Balance @@ -95,7 +95,7 @@ def transfer_extrinsic( amount, destination, keep_alive ) - call = subtensor.substrate.compose_call( + call = subtensor.compose_call( call_module="Balances", call_function=call_function, call_params=call_params, @@ -109,7 +109,7 @@ def transfer_extrinsic( period=period, raise_error=raise_error, ) - response.transaction_fee = fee + response.transaction_tao_fee = fee if response.success: block_hash = subtensor.get_block_hash() diff --git a/bittensor/core/extrinsics/unstaking.py b/bittensor/core/extrinsics/unstaking.py index 11c19f1423..464ca65e23 100644 --- a/bittensor/core/extrinsics/unstaking.py +++ b/bittensor/core/extrinsics/unstaking.py @@ -3,6 +3,7 @@ from async_substrate_interface.errors import SubstrateRequestException from bittensor.core.errors import BalanceTypeError +from bittensor.core.extrinsics.params import UnstakingParams from bittensor.core.extrinsics.utils import get_old_stakes from bittensor.core.types import ExtrinsicResponse, UIDs from bittensor.utils import format_error_message @@ -77,54 +78,51 @@ def unstake_extrinsic( f"Not enough stake: {old_stake} to unstake: {amount} from hotkey: {hotkey_ss58}", ).with_log() - call_params = { - "hotkey": hotkey_ss58, - "netuid": netuid, - "amount_unstaked": amount.rao, - } - if safe_unstaking: pool = subtensor.subnet(netuid=netuid) - base_price = pool.price.tao - if pool.netuid == 0: - price_with_tolerance = base_price - else: - price_with_tolerance = base_price * (1 - rate_tolerance) + call_function = "remove_stake_limit" + + call_params = UnstakingParams.remove_stake_limit( + netuid=netuid, + hotkey_ss58=hotkey_ss58, + amount=amount, + allow_partial_stake=allow_partial_stake, + rate_tolerance=rate_tolerance, + pool=pool, + ) logging_message = ( f":satellite: [magenta]Safe Unstaking from:[/magenta] " f"netuid: [green]{netuid}[/green], amount: [green]{amount}[/green], " f"tolerance percentage: [green]{rate_tolerance * 100}%[/green], " - f"price limit: [green]{price_with_tolerance}[/green], " - f"original price: [green]{base_price}[/green], " + f"price limit: [green]{Balance.from_rao(call_params['limit_price'])}[/green], " + f"original price: [green]{pool.price.tao}[/green], " f"with partial unstake: [green]{allow_partial_stake}[/green] " f"on [blue]{subtensor.network}[/blue]" ) - limit_price = Balance.from_tao(price_with_tolerance).rao - call_params.update( - { - "limit_price": limit_price, - "allow_partial": allow_partial_stake, - } - ) - call_function = "remove_stake_limit" else: + call_function = "remove_stake" + call_params = UnstakingParams.remove_stake( + netuid=netuid, + hotkey_ss58=hotkey_ss58, + amount=amount, + ) logging_message = ( f":satellite: [magenta]Unstaking from:[/magenta] " f"netuid: [green]{netuid}[/green], amount: [green]{amount}[/green] " f"on [blue]{subtensor.network}[/blue]" ) - call_function = "remove_stake" logging.debug(logging_message) - call = subtensor.substrate.compose_call( + call = subtensor.compose_call( call_module="SubtensorModule", call_function=call_function, call_params=call_params, ) + block_before = subtensor.block response = subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, @@ -136,8 +134,16 @@ def unstake_extrinsic( raise_error=raise_error, ) - if response.success: # If we successfully unstaked. - # We only wait here if we expect finalization. + if response.success: + sim_swap = subtensor.sim_swap( + origin_netuid=netuid, + destination_netuid=0, + 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 @@ -180,8 +186,8 @@ def unstake_extrinsic( def unstake_all_extrinsic( subtensor: "Subtensor", wallet: "Wallet", - hotkey_ss58: str, netuid: int, + hotkey_ss58: str, rate_tolerance: Optional[float] = 0.005, period: Optional[int] = None, raise_error: bool = False, @@ -193,8 +199,8 @@ def unstake_all_extrinsic( Parameters: subtensor: Subtensor instance. wallet: The wallet of the stake owner. - hotkey_ss58: The SS58 address of the hotkey to unstake from. netuid: The unique identifier of the subnet. + hotkey_ss58: The SS58 address of the hotkey to unstake from. rate_tolerance: The maximum allowed price change ratio when unstaking. For example, 0.005 = 0.5% maximum price decrease. If not passed (None), then unstaking goes without price limit. period: The number of blocks during which the transaction will remain valid after it's submitted. If the @@ -213,18 +219,15 @@ def unstake_all_extrinsic( ).success: return unlocked - call_params = { - "hotkey": hotkey_ss58, - "netuid": netuid, - "limit_price": None, - } - - if rate_tolerance: - current_price = subtensor.subnet(netuid=netuid).price - limit_price = current_price * (1 - rate_tolerance) - call_params.update({"limit_price": limit_price}) + pool = subtensor.subnet(netuid=netuid) if rate_tolerance else None + call_params = UnstakingParams.remove_stake_full_limit( + netuid=netuid, + hotkey_ss58=hotkey_ss58, + rate_tolerance=rate_tolerance, + pool=pool, + ) - call = subtensor.substrate.compose_call( + call = subtensor.compose_call( call_module="SubtensorModule", call_function="remove_stake_full_limit", call_params=call_params, diff --git a/bittensor/core/extrinsics/utils.py b/bittensor/core/extrinsics/utils.py index 6327e98e74..b1a94f749d 100644 --- a/bittensor/core/extrinsics/utils.py +++ b/bittensor/core/extrinsics/utils.py @@ -6,8 +6,7 @@ from bittensor.utils.balance import Balance if TYPE_CHECKING: - from scalecodec import GenericCall - from bittensor_wallet import Wallet, Keypair + from bittensor_wallet import Wallet from bittensor.core.chain_data import StakeInfo from bittensor.core.subtensor import Subtensor @@ -47,30 +46,6 @@ def get_old_stakes( ] -def get_extrinsic_fee( - subtensor: "Subtensor", - call: "GenericCall", - keypair: "Keypair", - netuid: Optional[int] = None, -): - """ - Get extrinsic fee for a given extrinsic call and keypair for a given SN's netuid. - - Parameters: - subtensor: The Subtensor instance. - call: The extrinsic call. - keypair: The keypair associated with the extrinsic. - netuid: The SN's netuid. - - Returns: - Balance object representing the extrinsic fee in RAO. - """ - payment_info = subtensor.substrate.get_payment_info(call=call, keypair=keypair) - return Balance.from_rao(amount=payment_info["partial_fee"]).set_unit( - netuid=netuid or 0 - ) - - def sudo_call_extrinsic( subtensor: "Subtensor", wallet: "Wallet", @@ -116,13 +91,13 @@ def sudo_call_extrinsic( ).success: return unlocked - call = subtensor.substrate.compose_call( + call = subtensor.compose_call( call_module=call_module, call_function=call_function, call_params=call_params, ) if not root_call: - call = subtensor.substrate.compose_call( + call = subtensor.compose_call( call_module="Sudo", call_function="sudo", call_params={"call": call}, diff --git a/bittensor/core/extrinsics/weights.py b/bittensor/core/extrinsics/weights.py index 934190c72d..a46f028224 100644 --- a/bittensor/core/extrinsics/weights.py +++ b/bittensor/core/extrinsics/weights.py @@ -4,6 +4,7 @@ from bittensor_drand import get_encrypted_commit +from bittensor.core.extrinsics.params import WeightsParams from bittensor.core.settings import version_as_int from bittensor.core.types import ExtrinsicResponse, Salt, UIDs, Weights from bittensor.utils import get_mechid_storage_index @@ -90,16 +91,16 @@ def commit_timelocked_weights_extrinsic( hotkey=wallet.hotkey.public_key, ) - call = subtensor.substrate.compose_call( + call = subtensor.compose_call( call_module="SubtensorModule", call_function="commit_timelocked_mechanism_weights", - call_params={ - "netuid": netuid, - "mecid": mechid, - "commit": commit_for_reveal, - "reveal_round": reveal_round, - "commit_reveal_version": commit_reveal_version, - }, + call_params=WeightsParams.commit_timelocked_mechanism_weights( + netuid=netuid, + mechid=mechid, + commit_for_reveal=commit_for_reveal, + reveal_round=reveal_round, + commit_reveal_version=commit_reveal_version, + ), ) response = subtensor.sign_and_send_extrinsic( call=call, @@ -183,14 +184,14 @@ def commit_weights_extrinsic( version_key=version_key, ) - call = subtensor.substrate.compose_call( + call = subtensor.compose_call( call_module="SubtensorModule", call_function="commit_mechanism_weights", - call_params={ - "netuid": netuid, - "mecid": mechid, - "commit_hash": commit_hash, - }, + call_params=WeightsParams.commit_mechanism_weights( + netuid=netuid, + mechid=mechid, + commit_hash=commit_hash, + ), ) response = subtensor.sign_and_send_extrinsic( call=call, @@ -263,17 +264,17 @@ def reveal_weights_extrinsic( # Convert, reformat and normalize uids and weights. uids, weights = convert_and_normalize_weights_and_uids(uids, weights) - call = subtensor.substrate.compose_call( + call = subtensor.compose_call( call_module="SubtensorModule", call_function="reveal_mechanism_weights", - call_params={ - "netuid": netuid, - "mecid": mechid, - "uids": uids, - "values": weights, - "salt": salt, - "version_key": version_key, - }, + call_params=WeightsParams.reveal_mechanism_weights( + netuid=netuid, + mechid=mechid, + uids=uids, + weights=weights, + salt=salt, + version_key=version_key, + ), ) response = subtensor.sign_and_send_extrinsic( call=call, @@ -344,16 +345,16 @@ def set_weights_extrinsic( # Convert, reformat and normalize uids and weights. uids, weights = convert_and_normalize_weights_and_uids(uids, weights) - call = subtensor.substrate.compose_call( + call = subtensor.compose_call( call_module="SubtensorModule", call_function="set_mechanism_weights", - call_params={ - "netuid": netuid, - "mecid": mechid, - "dests": uids, - "weights": weights, - "version_key": version_key, - }, + call_params=WeightsParams.set_mechanism_weights( + netuid=netuid, + mechid=mechid, + uids=uids, + weights=weights, + version_key=version_key, + ), ) response = subtensor.sign_and_send_extrinsic( call=call, diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 17ddd0006f..a6e0087ca9 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -10,6 +10,7 @@ from async_substrate_interface.types import ScaleObj from async_substrate_interface.utils.storage import StorageKey from bittensor_drand import get_encrypted_commitment +from bittensor_wallet.utils import SS58_FORMAT from bittensor.core.async_subtensor import ProposalVoteData from bittensor.core.axon import Axon @@ -21,6 +22,7 @@ NeuronInfo, NeuronInfoLite, SelectiveMetagraphIndex, + SimSwapResult, StakeInfo, SubnetInfo, SubnetIdentity, @@ -52,6 +54,7 @@ swap_stake_extrinsic, move_stake_extrinsic, ) +from bittensor.core.extrinsics.params.transfer import get_transfer_fn_params from bittensor.core.extrinsics.registration import ( burned_register_extrinsic, register_extrinsic, @@ -76,7 +79,6 @@ unstake_extrinsic, unstake_multiple_extrinsic, ) -from bittensor.core.extrinsics.utils import get_extrinsic_fee from bittensor.core.extrinsics.weights import ( commit_timelocked_weights_extrinsic, commit_weights_extrinsic, @@ -84,7 +86,6 @@ set_weights_extrinsic, ) from bittensor.core.metagraph import Metagraph -from bittensor_wallet.utils import SS58_FORMAT from bittensor.core.settings import ( version_as_int, TAO_APP_BLOCK_EXPLORER, @@ -104,7 +105,6 @@ decode_hex_identity_dict, format_error_message, get_caller_name, - get_transfer_fn_params, get_mechid_storage_index, is_valid_ss58_address, u16_normalized_float, @@ -124,12 +124,9 @@ price_to_tick, LiquidityPosition, ) -from bittensor.utils.weight_utils import ( - U16_MAX, -) if TYPE_CHECKING: - from bittensor_wallet import Wallet + from bittensor_wallet import Keypair, Wallet from async_substrate_interface.sync_substrate import QueryMapResult from scalecodec.types import GenericCall @@ -244,6 +241,14 @@ def _get_substrate( ) def determine_block_hash(self, block: Optional[int]) -> Optional[str]: + """Determine the appropriate block hash based on the provided block. + + Parameters: + block: The block number to query. + + Returns: + The block hash if one can be determined, None otherwise. + """ if block is None: return None else: @@ -301,6 +306,78 @@ def get_hyperparameter( def block(self) -> int: return self.get_current_block() + def sim_swap( + self, + origin_netuid: int, + destination_netuid: int, + amount: "Balance", + block: Optional[int] = None, + ) -> SimSwapResult: + """ + Hits the SimSwap Runtime API to calculate the fee and result for a given transaction. The SimSwapResult contains + the staking fees and expected returned amounts of a given transaction. This does not include the transaction + (extrinsic) fee. + + Args: + origin_netuid: Netuid of the source subnet (0 if add stake). + destination_netuid: Netuid of the destination subnet. + amount: Amount to stake operation. + block: The blockchain block number at which to perform the query. + + Returns: + SimSwapResult object representing the result. + """ + check_balance_amount(amount) + if origin_netuid > 0 and destination_netuid > 0: + # for cross-subnet moves where neither origin nor destination is root + intermediate_result_ = self.query_runtime_api( + runtime_api="SwapRuntimeApi", + method="sim_swap_alpha_for_tao", + params={"netuid": origin_netuid, "alpha": amount.rao}, + block=block, + ) + sn_price = self.get_subnet_price(origin_netuid, block=block) + intermediate_result = SimSwapResult.from_dict( + intermediate_result_, origin_netuid + ) + result = SimSwapResult.from_dict( + self.query_runtime_api( + runtime_api="SwapRuntimeApi", + method="sim_swap_tao_for_alpha", + params={ + "netuid": destination_netuid, + "tao": intermediate_result.tao_amount.rao, + }, + block=block, + ), + origin_netuid, + ) + secondary_fee = (result.tao_fee / sn_price.tao).set_unit(origin_netuid) + result.alpha_fee = result.alpha_fee + secondary_fee + return result + elif origin_netuid > 0: + # dynamic to tao + return SimSwapResult.from_dict( + self.query_runtime_api( + runtime_api="SwapRuntimeApi", + method="sim_swap_alpha_for_tao", + params={"netuid": origin_netuid, "alpha": amount.rao}, + block=block, + ), + origin_netuid, + ) + else: + # tao to dynamic or unstaked to staked tao (SN0) + return SimSwapResult.from_dict( + self.query_runtime_api( + runtime_api="SwapRuntimeApi", + method="sim_swap_tao_for_alpha", + params={"netuid": destination_netuid, "tao": amount.rao}, + block=block, + ), + destination_netuid, + ) + # Subtensor queries =========================================================================================== def query_constant( @@ -1989,27 +2066,6 @@ def get_stake( return Balance.from_rao(int(stake)).set_unit(netuid=netuid) - # TODO: update related with fee calculation - def get_stake_add_fee( - self, - amount: Balance, - netuid: int, - block: Optional[int] = None, - ) -> Balance: - """ - Calculates the fee for adding new stake to a hotkey. - - Parameters: - amount: Amount of stake to add in TAO - netuid: Netuid of subnet - block: Block number at which to perform the calculation - - Returns: - The calculated stake fee as a Balance object - """ - check_balance_amount(amount) - return self.get_stake_operations_fee(amount=amount, netuid=netuid, block=block) - def get_stake_for_coldkey_and_hotkey( self, coldkey_ss58: str, @@ -2093,54 +2149,56 @@ def get_stake_for_hotkey( get_hotkey_stake = get_stake_for_hotkey - # TODO: update related with fee calculation - def get_stake_movement_fee( + def get_stake_add_fee( self, amount: Balance, - origin_netuid: int, + netuid: int, block: Optional[int] = None, ) -> Balance: """ - Calculates the fee for moving stake between hotkeys/subnets/coldkeys. + Calculates the fee for adding new stake to a hotkey. Parameters: - amount: Amount of stake to move in TAO - origin_netuid: Netuid of origin subnet + amount: Amount of stake to add in TAO + netuid: Netuid of subnet block: Block number at which to perform the calculation Returns: - The calculated stake fee as a Balance object + The calculated stake fee as a Balance object in TAO. """ check_balance_amount(amount) - return self.get_stake_operations_fee( - amount=amount, netuid=origin_netuid, block=block + sim_swap_result = self.sim_swap( + origin_netuid=0, destination_netuid=netuid, amount=amount, block=block ) + return sim_swap_result.tao_fee - def get_stake_operations_fee( + def get_stake_movement_fee( self, + origin_netuid: int, + destination_netuid: int, amount: Balance, - netuid: int, block: Optional[int] = None, - ): - """Returns fee for any stake operation in specified subnet. + ) -> Balance: + """ + Calculates the fee for moving stake between hotkeys/subnets/coldkeys. Parameters: - amount: Amount of stake to add in Alpha/TAO. - netuid: Netuid of subnet. - block: Block number at which to perform the calculation. + origin_netuid: Netuid of source subnet. + destination_netuid: Netuid of the destination subnet. + amount: Amount of stake to move. + block: The block number for which the children are to be retrieved. Returns: - The calculated stake fee as a Balance object. + The calculated stake fee as a Balance object """ check_balance_amount(amount) - block_hash = self.determine_block_hash(block=block) - result = self.substrate.query( - module="Swap", - storage_function="FeeRate", - params=[netuid], - block_hash=block_hash, + sim_swap_result = self.sim_swap( + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + amount=amount, + block=block, ) - return amount * (result.value / U16_MAX) + return sim_swap_result.tao_fee def get_stake_weight(self, netuid: int, block: Optional[int] = None) -> list[float]: """ @@ -2424,7 +2482,6 @@ def get_total_subnets(self, block: Optional[int] = None) -> Optional[int]: ) return getattr(result, "value", None) - # TODO: update related with fee calculation def get_transfer_fee( self, wallet: "Wallet", @@ -2455,7 +2512,7 @@ def get_transfer_fee( call_params: dict[str, Union[int, str, bool]] call_function, call_params = get_transfer_fn_params(amount, dest, keep_alive) - call = self.substrate.compose_call( + call = self.compose_call( call_module="Balances", call_function=call_function, call_params=call_params, @@ -2471,26 +2528,31 @@ def get_transfer_fee( return Balance.from_rao(payment_info["partial_fee"]) - # TODO: update related with fee calculation def get_unstake_fee( self, - amount: Balance, netuid: int, + amount: Balance, block: Optional[int] = None, ) -> Balance: """ Calculates the fee for unstaking from a hotkey. Parameters: - amount: Amount of stake to unstake in TAO - netuid: Netuid of subnet - block: Block number at which to perform the calculation + netuid: The unique identifier of the subnet. + amount: Amount of stake to unstake in TAO. + block: Block number at which to perform the calculation. Returns: - The calculated stake fee as a Balance object + The calculated stake fee as a Balance object in Alpha. """ check_balance_amount(amount) - return self.get_stake_operations_fee(amount=amount, netuid=netuid, block=block) + sim_swap_result = self.sim_swap( + origin_netuid=netuid, + destination_netuid=0, + amount=amount, + block=block, + ) + return sim_swap_result.alpha_fee.set_unit(netuid=netuid) def get_vote_data( self, proposal_hash: str, block: Optional[int] = None @@ -3179,7 +3241,103 @@ def weights_rate_limit( ) return None if call is None else int(call) - # Extrinsics helper ================================================================================================ + # Extrinsics helpers =============================================================================================== + + def validate_extrinsic_params( + self, + call_module: str, + call_function: str, + call_params: dict[str, Any], + block: Optional[int] = None, + ): + """ + Validate and filter extrinsic parameters against on-chain metadata. + + This method checks that the provided parameters match the expected signature of the given extrinsic (module and + function) as defined in the Substrate metadata. It raises explicit errors for missing or invalid parameters and + silently ignores any extra keys not present in the function definition. + + Args: + call_module: The pallet name, e.g. "SubtensorModule" or "AdminUtils". + call_function: The extrinsic function name, e.g. "set_weights" or "sudo_set_tempo". + call_params: A dictionary of parameters to validate. + block: Optional block number to query metadata from. If not provided, the latest metadata is used. + + Returns: + A filtered dictionary containing only the parameters that are valid for the specified extrinsic. + + Raises: + ValueError: If the given module or function is not found in the chain metadata. + KeyError: If one or more required parameters are missing. + + Notes: + This method does not compose or submit the extrinsic. It only ensures that `call_params` conforms to the + expected schema derived from on-chain metadata. + """ + block_hash = self.determine_block_hash(block=block) + + func_meta = self.substrate.get_metadata_call_function( + module_name=call_module, + call_function_name=call_function, + block_hash=block_hash, + ) + + if not func_meta: + raise ValueError( + f"Call {call_module}.{call_function} not found in chain metadata." + ) + + # Expected params from metadata + expected_params = func_meta.get_param_info() + provided_params = {} + + # Validate and filter parameters + for param_name in expected_params.keys(): + if param_name not in call_params: + raise KeyError(f"Missing required parameter: '{param_name}'") + provided_params[param_name] = call_params[param_name] + + # Warn about extra params not defined in metadata + extra_params = set(call_params.keys()) - set(expected_params.keys()) + if extra_params: + logging.debug( + f"Ignoring extra parameters for {call_module}.{call_function}: {extra_params}." + ) + return provided_params + + def compose_call( + self, + call_module: str, + call_function: str, + call_params: dict[str, Any], + block: Optional[int] = None, + ) -> "GenericCall": + """ + Dynamically compose a GenericCall using on-chain Substrate metadata after validating the provided parameters. + + Args: + call_module: Pallet name (e.g. "SubtensorModule", "AdminUtils"). + call_function: Function name (e.g. "set_weights", "sudo_set_tempo"). + call_params: Dictionary of parameters for the call. + block: Block number for querying metadata. + + Returns: + GenericCall: Composed call object ready for extrinsic submission. + """ + call_params = self.validate_extrinsic_params( + call_module, call_function, call_params, block + ) + block_hash = self.determine_block_hash(block=block) + logging.debug( + f"Composing GenericCall -> {call_module}.{call_function} " + f"with params: {call_params}." + ) + return self.substrate.compose_call( + call_module=call_module, + call_function=call_function, + call_params=call_params, + block_hash=block_hash, + ) def sign_and_send_extrinsic( self, @@ -3242,8 +3400,8 @@ def sign_and_send_extrinsic( if period is not None: extrinsic_data["era"] = {"period": period} - extrinsic_response.extrinsic_fee = get_extrinsic_fee( - subtensor=self, call=call, keypair=signing_keypair + extrinsic_response.extrinsic_fee = self.get_extrinsic_fee( + call=call, keypair=signing_keypair ) extrinsic_response.extrinsic = self.substrate.create_signed_extrinsic( **extrinsic_data @@ -3286,6 +3444,27 @@ def sign_and_send_extrinsic( extrinsic_response.error = error return extrinsic_response + def get_extrinsic_fee( + self, + call: "GenericCall", + keypair: "Keypair", + ): + """ + Get extrinsic fee for a given extrinsic call and keypair for a given SN's netuid. + + Parameters: + call: The extrinsic GenericCall. + keypair: The keypair associated with the extrinsic. + + Returns: + Balance object representing the extrinsic fee in RAO. + + Note: + To create the GenericCall object use `compose_call` method with proper parameters. + """ + payment_info = self.substrate.get_payment_info(call=call, keypair=keypair) + return Balance.from_rao(amount=payment_info["partial_fee"]) + # Extrinsics ======================================================================================================= def add_stake( @@ -3643,10 +3822,10 @@ def modify_liquidity( def move_stake( self, wallet: "Wallet", - origin_hotkey_ss58: str, origin_netuid: int, - destination_hotkey_ss58: str, + origin_hotkey_ss58: str, destination_netuid: int, + destination_hotkey_ss58: str, amount: Optional[Balance] = None, move_all_stake: bool = False, period: Optional[int] = None, @@ -3659,10 +3838,10 @@ def move_stake( Parameters: wallet: The wallet to move stake from. - origin_hotkey_ss58: The SS58 address of the source hotkey. origin_netuid: The netuid of the source subnet. - destination_hotkey_ss58: The SS58 address of the destination hotkey. + origin_hotkey_ss58: The SS58 address of the source hotkey. destination_netuid: The netuid of the destination subnet. + destination_hotkey_ss58: The SS58 address of the destination hotkey. amount: Amount of stake to move. move_all_stake: If true, moves all stake from the source hotkey to the destination hotkey. period: The number of blocks during which the transaction will remain valid after it's submitted. If the @@ -3679,10 +3858,10 @@ def move_stake( return move_stake_extrinsic( subtensor=self, wallet=wallet, - origin_hotkey_ss58=origin_hotkey_ss58, origin_netuid=origin_netuid, - destination_hotkey_ss58=destination_hotkey_ss58, + origin_hotkey_ss58=origin_hotkey_ss58, destination_netuid=destination_netuid, + destination_hotkey_ss58=destination_hotkey_ss58, amount=amount, move_all_stake=move_all_stake, period=period, @@ -4019,8 +4198,8 @@ def set_auto_stake( def set_children( self, wallet: "Wallet", - hotkey_ss58: str, netuid: int, + hotkey_ss58: str, children: list[tuple[float, str]], period: Optional[int] = None, raise_error: bool = False, @@ -4760,8 +4939,8 @@ def unstake( def unstake_all( self, wallet: "Wallet", - hotkey_ss58: str, netuid: int, + hotkey_ss58: str, rate_tolerance: Optional[float] = 0.005, period: Optional[int] = None, raise_error: bool = False, @@ -4772,8 +4951,8 @@ def unstake_all( Parameters: wallet: The wallet of the stake owner. - hotkey_ss58: The SS58 address of the hotkey to unstake from. netuid: The unique identifier of the subnet. + hotkey_ss58: The SS58 address of the hotkey to unstake from. rate_tolerance: The maximum allowed price change ratio when unstaking. For example, 0.005 = 0.5% maximum price decrease. If not passed (None), then unstaking goes without price limit. period: The number of blocks during which the transaction will remain valid after it's submitted. If @@ -4827,8 +5006,8 @@ def unstake_all( return unstake_all_extrinsic( subtensor=self, wallet=wallet, - hotkey_ss58=hotkey_ss58, netuid=netuid, + hotkey_ss58=hotkey_ss58, rate_tolerance=rate_tolerance, period=period, raise_error=raise_error, diff --git a/bittensor/core/types.py b/bittensor/core/types.py index 08280c6d2a..98ec97922f 100644 --- a/bittensor/core/types.py +++ b/bittensor/core/types.py @@ -10,12 +10,13 @@ from bittensor.core.chain_data import NeuronInfo, NeuronInfoLite from bittensor.core.config import Config from bittensor.utils import ( - Certificate, determine_chain_endpoint_and_network, get_caller_name, format_error_message, networking, unlock_key, + Certificate, + UnlockStatus, ) from bittensor.utils.btlogging import logging @@ -305,7 +306,8 @@ class ExtrinsicResponse: extrinsic: The raw extrinsic object used in the call, if available. extrinsic_fee: The fee charged by the extrinsic, if available. extrinsic_receipt: The receipt object of the submitted extrinsic. - transaction_fee: The fee charged by the transaction (e.g., fee for add_stake or transfer_stake), if available. + transaction_tao_fee: TAO fee charged by the transaction in TAO (e.g., fee for add_stake), if available. + transaction_alpha_fee: Alpha fee charged by the transaction (e.g., fee for transfer_stake), if available. error: Captures the underlying exception if the extrinsic failed, otherwise `None`. data: Arbitrary data returned from the extrinsic, such as decoded events, balance or another extra context. @@ -332,7 +334,8 @@ class ExtrinsicResponse: message: Successfully registered subnet extrinsic_function: register_subnet_extrinsic extrinsic: {'account_id': '0xd43593c715fdd31c... - extrinsic_fee: τ1.0 + transaction_tao_fee: τ1.0 + transaction_alpha_fee: 1.0β extrinsic_receipt: Extrinsic Receipt data of of the submitted extrinsic transaction_fee: τ1.0 error: None @@ -357,7 +360,8 @@ class ExtrinsicResponse: extrinsic_receipt: Optional[Union["AsyncExtrinsicReceipt", "ExtrinsicReceipt"]] = ( None ) - transaction_fee: Optional["Balance"] = None + transaction_tao_fee: Optional["Balance"] = None + transaction_alpha_fee: Optional["Balance"] = None error: Optional[Exception] = None data: Optional[Any] = None @@ -379,7 +383,8 @@ def __str__(self): f"\textrinsic: {self.extrinsic}\n" f"\textrinsic_fee: {self.extrinsic_fee}\n" f"\textrinsic_receipt: {_extrinsic_receipt}" - f"\ttransaction_fee: {self.transaction_fee}\n" + f"\ttransaction_tao_fee: {self.transaction_tao_fee}\n" + f"\ttransaction_alpha_fee: {self.transaction_alpha_fee}\n" f"\tdata: {self.data}\n" f"\terror: {self.error}" ) @@ -394,9 +399,12 @@ def as_dict(self) -> dict: "message": self.message, "extrinsic_function": self.extrinsic_function, "extrinsic": self.extrinsic, - "extrinsic_fee": str(self.extrinsic_fee) if self.extrinsic_fee else None, - "transaction_fee": str(self.transaction_fee) - if self.transaction_fee + "extrinsic_fee": self.extrinsic_fee.rao if self.extrinsic_fee else None, + "transaction_tao_fee": self.transaction_tao_fee.rao + if self.transaction_tao_fee + else None, + "transaction_alpha_fee": str(self.transaction_alpha_fee) + if self.transaction_alpha_fee else None, "extrinsic_receipt": self.extrinsic_receipt, "error": str(self.error) if self.error else None, @@ -413,7 +421,8 @@ def __eq__(self, other: Any) -> bool: and self.extrinsic_function == other.extrinsic_function and self.extrinsic == other.extrinsic and self.extrinsic_fee == other.extrinsic_fee - and self.transaction_fee == other.transaction_fee + and self.transaction_tao_fee == other.transaction_tao_fee + and self.transaction_alpha_fee == other.transaction_alpha_fee and self.extrinsic_receipt == other.extrinsic_receipt and self.error == other.error and self.data == other.data @@ -450,15 +459,27 @@ def unlock_wallet( Parameters: wallet: Bittensor Wallet instance. raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - unlock_type: The key type, 'coldkey' or 'hotkey'. + unlock_type: The key type, 'coldkey' or 'hotkey'. Or 'both' to check both. nonce_key: Key used for generating nonce in extrinsic function. Returns: Extrinsic Response is used to check if the key is unlocked. + + Note: + When an extrinsic is signed with the coldkey but internally references or uses the hotkey, both keypairs + must be validated. Passing unlock_type='both' ensures that authentication is performed against both the + coldkey and hotkey. """ - unlock = unlock_key(wallet, unlock_type=unlock_type, raise_error=raise_error) - if not unlock.success: - logging.error(unlock.message) + both = ["coldkey", "hotkey"] + keys = [unlock_type] if unlock_type in both else both + unlock = UnlockStatus(False, "") + + for unlock_type in keys: + unlock = unlock_key( + wallet, unlock_type=unlock_type, raise_error=raise_error + ) + if not unlock.success: + logging.error(unlock.message) # If extrinsic uses `unlock_type` and `nonce_key` and `nonce_key` is not public, we need to check the # availability of both keys. diff --git a/bittensor/extras/dev_framework/subnet.py b/bittensor/extras/dev_framework/subnet.py index af6f5f03df..c68d48a005 100644 --- a/bittensor/extras/dev_framework/subnet.py +++ b/bittensor/extras/dev_framework/subnet.py @@ -114,11 +114,7 @@ def execute_one(self, step: Union[STEPS, tuple]) -> ExtrinsicResponse: if call_function.startswith("sudo_") and sudo_call is None: sudo_call = True - self._check_set_hp( - function_module=call_module, - function_name=call_function, - function_params=call_params, - ) + response = self.set_hyperparameter( sudo_or_owner_wallet=sudo_or_owner_wallet, call_function=call_function, @@ -179,11 +175,7 @@ async def async_execute_one(self, step: Union[STEPS, tuple]) -> ExtrinsicRespons if call_function.startswith("sudo_") and sudo_call is None: sudo_call = True - self._check_set_hp( - function_module=call_module, - function_name=call_function, - function_params=call_params, - ) + response = await self.async_set_hyperparameter( sudo_or_owner_wallet=sudo_or_owner_wallet, call_function=call_function, @@ -478,33 +470,6 @@ def _add_call_record(self, operation: str, response: ExtrinsicResponse): """Add extrinsic response to the calls list.""" self._calls.append(CALL_RECORD(len(self._calls), operation, response)) - def _check_set_hp( - self, - function_module: str, - function_name: str, - function_params: Optional[dict] = None, - ): - """Check if the function exists in the Subtensor node and parameters are provided for the function correctly.""" - pallet = self.s.substrate.metadata.get_metadata_pallet(function_module) - assert pallet is not None, f"Pallet {function_module} is not found." - functions = getattr(pallet.calls, "value", None) - functions_call = [f for f in functions if f.get("name", None) == function_name] - function_call = functions_call[0] if functions_call else None - assert function_call is not None, ( - f"Function {function_name} is not found in pallet {function_module}." - ) - function_call_fields = function_call.get("fields", []) - parameters = [field.get("name") for field in function_call_fields] - assert parameters is not None, ( - f"Parameters are required for function {function_name}." - ) - if "netuid" in parameters and not function_params.get("netuid", None): - function_params.update({"netuid": self._netuid}) - assert len(parameters) == len(function_params) is not False, ( - f"Not all parameters {function_params} where provided for function {function_name}. " - f"All required parameters: {parameters}." - ) - def _check_response(self, response: ExtrinsicResponse) -> bool: """Check if the call was successful.""" if response.success: diff --git a/bittensor/extras/subtensor_api/__init__.py b/bittensor/extras/subtensor_api/__init__.py index 7f9a8e500a..e55c46df0c 100644 --- a/bittensor/extras/subtensor_api/__init__.py +++ b/bittensor/extras/subtensor_api/__init__.py @@ -109,6 +109,7 @@ def __init__( self.determine_block_hash = self.inner_subtensor.determine_block_hash self.encode_params = self.inner_subtensor.encode_params + self.compose_call = self.inner_subtensor.compose_call self.sign_and_send_extrinsic = self.inner_subtensor.sign_and_send_extrinsic self.start_call = self.inner_subtensor.start_call self.wait_for_block = self.inner_subtensor.wait_for_block diff --git a/bittensor/extras/subtensor_api/extrinsics.py b/bittensor/extras/subtensor_api/extrinsics.py index 62c6c5c1b4..b428f5e247 100644 --- a/bittensor/extras/subtensor_api/extrinsics.py +++ b/bittensor/extras/subtensor_api/extrinsics.py @@ -12,6 +12,7 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.add_stake_multiple = subtensor.add_stake_multiple self.burned_register = subtensor.burned_register self.commit_weights = subtensor.commit_weights + self.get_extrinsic_fee = subtensor.get_extrinsic_fee self.modify_liquidity = subtensor.modify_liquidity self.move_stake = subtensor.move_stake self.register = subtensor.register @@ -35,3 +36,4 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.unstake = subtensor.unstake self.unstake_all = subtensor.unstake_all self.unstake_multiple = subtensor.unstake_multiple + self.validate_extrinsic_params = subtensor.validate_extrinsic_params diff --git a/bittensor/extras/subtensor_api/staking.py b/bittensor/extras/subtensor_api/staking.py index 470bd9eeb2..12f798fca7 100644 --- a/bittensor/extras/subtensor_api/staking.py +++ b/bittensor/extras/subtensor_api/staking.py @@ -19,11 +19,11 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): ) self.get_stake_info_for_coldkey = subtensor.get_stake_info_for_coldkey self.get_stake_movement_fee = subtensor.get_stake_movement_fee - self.get_stake_operations_fee = subtensor.get_stake_operations_fee self.get_stake_weight = subtensor.get_stake_weight self.get_unstake_fee = subtensor.get_unstake_fee self.move_stake = subtensor.move_stake self.set_auto_stake = subtensor.set_auto_stake + self.sim_swap = subtensor.sim_swap self.swap_stake = subtensor.swap_stake self.transfer_stake = subtensor.transfer_stake self.unstake = subtensor.unstake diff --git a/bittensor/extras/subtensor_api/utils.py b/bittensor/extras/subtensor_api/utils.py index 8f4c500801..762957145c 100644 --- a/bittensor/extras/subtensor_api/utils.py +++ b/bittensor/extras/subtensor_api/utils.py @@ -40,6 +40,7 @@ def add_legacy_methods(subtensor: "SubtensorApi"): subtensor.inner_subtensor.get_all_revealed_commitments ) subtensor.get_all_subnets_info = subtensor.inner_subtensor.get_all_subnets_info + subtensor.get_all_subnets_netuid = subtensor.inner_subtensor.get_all_subnets_netuid subtensor.get_auto_stakes = subtensor.inner_subtensor.get_auto_stakes subtensor.get_balance = subtensor.inner_subtensor.get_balance subtensor.get_balances = subtensor.inner_subtensor.get_balances @@ -62,6 +63,7 @@ def add_legacy_methods(subtensor: "SubtensorApi"): subtensor.get_existential_deposit = ( subtensor.inner_subtensor.get_existential_deposit ) + subtensor.get_extrinsic_fee = subtensor.inner_subtensor.get_extrinsic_fee subtensor.get_hotkey_owner = subtensor.inner_subtensor.get_hotkey_owner subtensor.get_hotkey_stake = subtensor.inner_subtensor.get_hotkey_stake subtensor.get_hyperparameter = subtensor.inner_subtensor.get_hyperparameter @@ -104,9 +106,6 @@ def add_legacy_methods(subtensor: "SubtensorApi"): subtensor.inner_subtensor.get_stake_info_for_coldkey ) subtensor.get_stake_movement_fee = subtensor.inner_subtensor.get_stake_movement_fee - subtensor.get_stake_operations_fee = ( - subtensor.inner_subtensor.get_stake_operations_fee - ) subtensor.get_stake_weight = subtensor.inner_subtensor.get_stake_weight subtensor.get_subnet_burn_cost = subtensor.inner_subtensor.get_subnet_burn_cost subtensor.get_subnet_hyperparameters = ( @@ -124,7 +123,6 @@ def add_legacy_methods(subtensor: "SubtensorApi"): subtensor.get_subnet_validator_permits = ( subtensor.inner_subtensor.get_subnet_validator_permits ) - subtensor.get_all_subnets_netuid = subtensor.inner_subtensor.get_all_subnets_netuid subtensor.get_timelocked_weight_commits = ( subtensor.inner_subtensor.get_timelocked_weight_commits ) @@ -189,6 +187,7 @@ def add_legacy_methods(subtensor: "SubtensorApi"): subtensor.sign_and_send_extrinsic = ( subtensor.inner_subtensor.sign_and_send_extrinsic ) + subtensor.sim_swap = subtensor.inner_subtensor.sim_swap subtensor.start_call = subtensor.inner_subtensor.start_call subtensor.state_call = subtensor.inner_subtensor.state_call subtensor.subnet = subtensor.inner_subtensor.subnet @@ -204,6 +203,9 @@ def add_legacy_methods(subtensor: "SubtensorApi"): subtensor.unstake = subtensor.inner_subtensor.unstake subtensor.unstake_all = subtensor.inner_subtensor.unstake_all subtensor.unstake_multiple = subtensor.inner_subtensor.unstake_multiple + subtensor.validate_extrinsic_params = ( + subtensor.inner_subtensor.validate_extrinsic_params + ) subtensor.wait_for_block = subtensor.inner_subtensor.wait_for_block subtensor.weights = subtensor.inner_subtensor.weights subtensor.weights_rate_limit = subtensor.inner_subtensor.weights_rate_limit diff --git a/bittensor/extras/subtensor_api/wallets.py b/bittensor/extras/subtensor_api/wallets.py index 12034ed641..b6822159ed 100644 --- a/bittensor/extras/subtensor_api/wallets.py +++ b/bittensor/extras/subtensor_api/wallets.py @@ -39,3 +39,4 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.get_transfer_fee = subtensor.get_transfer_fee self.get_unstake_fee = subtensor.get_unstake_fee self.set_children = subtensor.set_children + self.transfer = subtensor.transfer diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index f821928842..c4ce11e924 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -486,37 +486,6 @@ def deprecated_message(message: str) -> None: warnings.warn(message=message, category=DeprecationWarning, stacklevel=2) -def get_transfer_fn_params( - amount: Optional["Balance"], destination: str, keep_alive: bool -) -> tuple[str, dict[str, Union[str, int, bool]]]: - """ - Helper function to get the transfer call function and call params, depending on the value and keep_alive flag - provided. - - Parameters: - amount: the amount of Tao to transfer. `None` if transferring all. - destination: the destination SS58 of the transfer - keep_alive: whether to enforce a retention of the existential deposit in the account after transfer. - - Returns: - tuple[call function, call params] - """ - call_params = {"dest": destination} - if amount is None: - call_function = "transfer_all" - if keep_alive: - call_params["keep_alive"] = True - else: - call_params["keep_alive"] = False - else: - call_params["value"] = amount.rao - if keep_alive: - call_function = "transfer_keep_alive" - else: - call_function = "transfer_allow_death" - return call_function, call_params - - def get_function_name() -> str: """Return the current function's name.""" return inspect.currentframe().f_back.f_code.co_name diff --git a/bittensor/utils/balance.py b/bittensor/utils/balance.py index f585dd0b11..a5b5056e2d 100644 --- a/bittensor/utils/balance.py +++ b/bittensor/utils/balance.py @@ -851,7 +851,7 @@ def rao(amount: int, netuid: int = 0) -> Balance: return Balance.from_rao(amount).set_unit(netuid) -def check_balance_amount(amount: Optional[Balance]) -> None: +def check_balance_amount(amount: Optional[Balance], allow_none: bool = True) -> None: """ Validate that the provided value is a Balance instance. @@ -860,6 +860,7 @@ def check_balance_amount(amount: Optional[Balance]) -> None: Args: amount: The value to validate. + allow_none: if False then a `BalanceTypeError` is raised if the value is None. Returns: None: Always returns None if validation passes. @@ -867,7 +868,7 @@ def check_balance_amount(amount: Optional[Balance]) -> None: Raises: BalanceTypeError: If amount is not a Balance instance and not None. """ - if amount is None: + if amount is None and allow_none is True: return None if not isinstance(amount, Balance): diff --git a/tests/e2e_tests/test_commit_reveal.py b/tests/e2e_tests/test_commit_reveal.py index f8877d0082..d3e260036a 100644 --- a/tests/e2e_tests/test_commit_reveal.py +++ b/tests/e2e_tests/test_commit_reveal.py @@ -276,9 +276,9 @@ async def test_commit_and_reveal_weights_cr4_async(async_subtensor, alice_wallet ) # Fetch current block and calculate next tempo for the subnet - current_block = await async_subtensor.chain.get_current_block() - upcoming_tempo = await async_subtensor.subnets.get_next_epoch_start_block( - alice_sn.netuid + current_block, upcoming_tempo = await asyncio.gather( + async_subtensor.chain.get_current_block(), + async_subtensor.subnets.get_next_epoch_start_block(alice_sn.netuid) ) logging.console.info( f"Checking if window is too low with Current block: {current_block}, next tempo: {upcoming_tempo}" @@ -288,15 +288,6 @@ async def test_commit_and_reveal_weights_cr4_async(async_subtensor, alice_wallet if upcoming_tempo - current_block < 6: await alice_sn.async_wait_next_epoch() - current_block = await async_subtensor.chain.get_current_block() - latest_drand_round = await async_subtensor.chain.last_drand_round() - upcoming_tempo = await async_subtensor.subnets.get_next_epoch_start_block( - alice_sn.netuid - ) - logging.console.info( - f"Post first wait_interval (to ensure window isn't too low): {current_block}, next tempo: {upcoming_tempo}, drand: {latest_drand_round}" - ) - for mechid in range(TESTED_MECHANISMS): logging.console.info( f"[magenta]Testing subnet mechanism: {alice_sn.netuid}.{mechid}[/magenta]" @@ -310,7 +301,7 @@ async def test_commit_and_reveal_weights_cr4_async(async_subtensor, alice_wallet uids=weight_uids, weights=weight_vals, wait_for_inclusion=True, - wait_for_finalization=True, + wait_for_finalization=False, block_time=BLOCK_TIME, period=16, ) @@ -322,16 +313,17 @@ async def test_commit_and_reveal_weights_cr4_async(async_subtensor, alice_wallet # Parse expected reveal_round expected_reveal_round = response.data.get("reveal_round") logging.console.success( - f"Successfully set weights: uids {weight_uids}, weights {weight_vals}, reveal_round: {expected_reveal_round}" + f"Successfully set weights: uids {weight_uids}, weights {weight_vals}, expected reveal round: {expected_reveal_round}" ) # Fetch current commits pending on the chain - await async_subtensor.wait_for_block(await async_subtensor.block + 1) + await async_subtensor.wait_for_block() - commits_on_chain = ( - await async_subtensor.commitments.get_timelocked_weight_commits( + commits_on_chain, weights = await asyncio.gather( + async_subtensor.commitments.get_timelocked_weight_commits( netuid=alice_sn.netuid, mechid=mechid - ) + ), + async_subtensor.subnets.weights(netuid=alice_sn.netuid, mechid=mechid) ) address, commit_block, commit, reveal_round = commits_on_chain[0] @@ -340,10 +332,7 @@ async def test_commit_and_reveal_weights_cr4_async(async_subtensor, alice_wallet assert address == alice_wallet.hotkey.ss58_address # Ensure no weights are available as of now - assert ( - await async_subtensor.subnets.weights(netuid=alice_sn.netuid, mechid=mechid) - == [] - ) + assert weights == [], "Weights already available." logging.console.success("No weights are available before next epoch.") # 5 is safety drand offset @@ -365,7 +354,7 @@ async def test_commit_and_reveal_weights_cr4_async(async_subtensor, alice_wallet logging.console.info( f"Latest drand round: {latest_drand_round}, waiting for next round..." ) - # drand round is 2 + # drand round is 3 seconds await asyncio.sleep(3) # Fetch weights on the chain as they should be revealed now @@ -373,7 +362,6 @@ async def test_commit_and_reveal_weights_cr4_async(async_subtensor, alice_wallet netuid=alice_sn.netuid, mechid=mechid ) assert subnet_weights != [], "Weights are not available yet." - logging.console.info(f"Revealed weights: {subnet_weights}") revealed_weights = subnet_weights[0][1] diff --git a/tests/e2e_tests/test_delegate.py b/tests/e2e_tests/test_delegate.py index 8c1f849ea1..583225a263 100644 --- a/tests/e2e_tests/test_delegate.py +++ b/tests/e2e_tests/test_delegate.py @@ -735,7 +735,7 @@ def test_get_vote_data(subtensor, alice_wallet): success, message = propose( subtensor=subtensor, wallet=alice_wallet, - proposal=subtensor.substrate.compose_call( + proposal=subtensor.compose_call( call_module="Triumvirate", call_function="set_members", call_params={ @@ -839,7 +839,7 @@ async def test_get_vote_data_async(async_subtensor, alice_wallet): success, message = await async_propose( subtensor=async_subtensor, wallet=alice_wallet, - proposal=await async_subtensor.substrate.compose_call( + proposal=await async_subtensor.compose_call( call_module="Triumvirate", call_function="set_members", call_params={ diff --git a/tests/e2e_tests/test_stake_fee.py b/tests/e2e_tests/test_stake_fee.py index 39b2fb522e..2a045ac355 100644 --- a/tests/e2e_tests/test_stake_fee.py +++ b/tests/e2e_tests/test_stake_fee.py @@ -1,5 +1,6 @@ import pytest +from bittensor.utils.btlogging import logging from bittensor.utils.balance import Balance from tests.e2e_tests.utils import TestSubnet, REGISTER_SUBNET @@ -16,16 +17,20 @@ def test_stake_fee_api(subtensor, alice_wallet, bob_wallet): - Moving stake between hotkeys/subnets/coldkeys """ root_netuid = 0 - stake_amount = Balance.from_tao(100) # 100 TAO - min_stake_fee = Balance.from_tao(0.050354772) + stake_amount = Balance.from_tao(1) # 1 TAO + min_stake_fee = Balance.from_tao(0.000503547) - sn = TestSubnet(subtensor) - sn.execute_one(REGISTER_SUBNET(alice_wallet)) + sn2 = TestSubnet(subtensor) + sn2.execute_one(REGISTER_SUBNET(alice_wallet)) + + # Test cross-subnet movement + sn3 = TestSubnet(subtensor) + sn3.execute_one(REGISTER_SUBNET(bob_wallet)) # Test add_stake fee stake_fee_0 = subtensor.staking.get_stake_add_fee( amount=stake_amount, - netuid=sn.netuid, + netuid=sn2.netuid, ) assert isinstance(stake_fee_0, Balance), "Stake fee should be a Balance object." assert stake_fee_0 == min_stake_fee, ( @@ -34,67 +39,59 @@ def test_stake_fee_api(subtensor, alice_wallet, bob_wallet): # Test unstake fee unstake_fee_root = subtensor.staking.get_unstake_fee( - amount=stake_amount, netuid=root_netuid, + amount=stake_amount, ) assert isinstance(unstake_fee_root, Balance), ( "Stake fee should be a Balance object." ) - assert unstake_fee_root == min_stake_fee, ( - "Root unstake fee should be equal the minimum stake fee." + assert unstake_fee_root == Balance.from_tao(0), ( + "Root unstake fee should be equal o TAO fee." ) # Test various stake movement scenarios movement_scenarios = [ - # Move from root to non-root { + "title": "Move from root to non-root", "origin_netuid": root_netuid, + "destination_netuid": sn2.netuid, "stake_fee": min_stake_fee, }, - # Move between hotkeys on root { + "title": "Move between hotkeys on root", "origin_netuid": root_netuid, + "destination_netuid": root_netuid, "stake_fee": 0, }, - # Move between coldkeys on root { + "title": "Move between coldkeys on root", "origin_netuid": root_netuid, + "destination_netuid": root_netuid, "stake_fee": 0, }, - # Move between coldkeys on non-root { - "origin_netuid": sn.netuid, + "title": "Move between coldkeys on non-root", + "origin_netuid": sn2.netuid, + "destination_netuid": sn2.netuid, + "stake_fee": min_stake_fee, + }, + { + "title": "Move between different subnets", + "origin_netuid": sn2.netuid, + "destination_netuid": sn3.netuid, "stake_fee": min_stake_fee, }, ] for scenario in movement_scenarios: + logging.console.info(f"Scenario: {scenario.get('title')}") stake_fee = subtensor.staking.get_stake_movement_fee( + origin_netuid=scenario.get("origin_netuid"), + destination_netuid=scenario.get("destination_netuid"), amount=stake_amount, - origin_netuid=scenario["origin_netuid"], ) assert isinstance(stake_fee, Balance), "Stake fee should be a Balance object" - assert stake_fee >= scenario["stake_fee"], ( - "Stake fee should be greater than the minimum stake fee" - ) - - # Test cross-subnet movement - netuid2 = 3 - assert subtensor.subnets.register_subnet(alice_wallet), ( - "Unable to register the second subnet" - ) - assert subtensor.subnets.subnet_exists(netuid2), ( - "Second subnet wasn't created successfully" - ) - - stake_fee = subtensor.staking.get_stake_movement_fee( - amount=stake_amount, - origin_netuid=sn.netuid, - ) - assert isinstance(stake_fee, Balance), "Stake fee should be a Balance object" - assert stake_fee >= min_stake_fee, ( - "Stake fee should be greater than the minimum stake fee" - ) + assert scenario["stake_fee"] >= stake_fee @pytest.mark.asyncio @@ -110,16 +107,20 @@ async def test_stake_fee_api_async(async_subtensor, alice_wallet, bob_wallet): - Moving stake between hotkeys/subnets/coldkeys """ root_netuid = 0 - stake_amount = Balance.from_tao(100) # 100 TAO - min_stake_fee = Balance.from_tao(0.050354772) + stake_amount = Balance.from_tao(1) # 1 TAO + min_stake_fee = Balance.from_tao(0.000503547) - sn = TestSubnet(async_subtensor) - await sn.async_execute_one(REGISTER_SUBNET(alice_wallet)) + sn2 = TestSubnet(async_subtensor) + await sn2.async_execute_one(REGISTER_SUBNET(bob_wallet)) + + # Test cross-subnet movement + sn3 = TestSubnet(async_subtensor) + await sn3.async_execute_one(REGISTER_SUBNET(bob_wallet)) # Test add_stake fee stake_fee_0 = await async_subtensor.staking.get_stake_add_fee( amount=stake_amount, - netuid=sn.netuid, + netuid=sn2.netuid, ) assert isinstance(stake_fee_0, Balance), "Stake fee should be a Balance object." assert stake_fee_0 == min_stake_fee, ( @@ -128,64 +129,56 @@ async def test_stake_fee_api_async(async_subtensor, alice_wallet, bob_wallet): # Test unstake fee unstake_fee_root = await async_subtensor.staking.get_unstake_fee( - amount=stake_amount, netuid=root_netuid, + amount=stake_amount, ) assert isinstance(unstake_fee_root, Balance), ( "Stake fee should be a Balance object." ) - assert unstake_fee_root == min_stake_fee, ( + assert unstake_fee_root == Balance.from_tao(0), ( "Root unstake fee should be equal the minimum stake fee." ) # Test various stake movement scenarios movement_scenarios = [ - # Move from root to non-root { + "title": "Move from root to non-root", "origin_netuid": root_netuid, + "destination_netuid": sn2.netuid, "stake_fee": min_stake_fee, }, - # Move between hotkeys on root { + "title": "Move between hotkeys on root", "origin_netuid": root_netuid, + "destination_netuid": root_netuid, "stake_fee": 0, }, - # Move between coldkeys on root { + "title": "Move between coldkeys on root", "origin_netuid": root_netuid, + "destination_netuid": root_netuid, "stake_fee": 0, }, - # Move between coldkeys on non-root { - "origin_netuid": sn.netuid, + "title": "Move between coldkeys on non-root", + "origin_netuid": sn2.netuid, + "destination_netuid": sn2.netuid, + "stake_fee": min_stake_fee, + }, + { + "title": "Move between different subnets", + "origin_netuid": sn2.netuid, + "destination_netuid": sn3.netuid, "stake_fee": min_stake_fee, }, ] for scenario in movement_scenarios: + logging.console.info(f"Scenario: {scenario.get('title')}") stake_fee = await async_subtensor.staking.get_stake_movement_fee( + origin_netuid=scenario.get("origin_netuid"), + destination_netuid=scenario.get("destination_netuid"), amount=stake_amount, - origin_netuid=scenario["origin_netuid"], ) assert isinstance(stake_fee, Balance), "Stake fee should be a Balance object" - assert stake_fee >= scenario["stake_fee"], ( - "Stake fee should be greater than the minimum stake fee" - ) - - # Test cross-subnet movement - netuid2 = 3 - assert await async_subtensor.subnets.register_subnet(alice_wallet), ( - "Unable to register the second subnet" - ) - assert await async_subtensor.subnets.subnet_exists(netuid2), ( - "Second subnet wasn't created successfully" - ) - - stake_fee = await async_subtensor.staking.get_stake_movement_fee( - amount=stake_amount, - origin_netuid=sn.netuid, - ) - assert isinstance(stake_fee, Balance), "Stake fee should be a Balance object" - assert stake_fee >= min_stake_fee, ( - "Stake fee should be greater than the minimum stake fee" - ) + assert scenario["stake_fee"] >= stake_fee diff --git a/tests/e2e_tests/test_staking.py b/tests/e2e_tests/test_staking.py index 692bfe2704..0d17987864 100644 --- a/tests/e2e_tests/test_staking.py +++ b/tests/e2e_tests/test_staking.py @@ -1399,8 +1399,6 @@ async def test_move_stake_async(async_subtensor, alice_wallet, bob_wallet, dave_ logging.console.info(f"[orange]Dave stake after moving all: {dave_stake}[orange]") assert dave_stake.rao == CloseInValue(0, 0.00001) - print(">>> alice", alice_sn.calls) - print(">>> alice", alice_sn.calls) def test_transfer_stake(subtensor, alice_wallet, bob_wallet, dave_wallet): diff --git a/tests/e2e_tests/test_subtensor_functions.py b/tests/e2e_tests/test_subtensor_functions.py index b236f9bd53..15adb2244e 100644 --- a/tests/e2e_tests/test_subtensor_functions.py +++ b/tests/e2e_tests/test_subtensor_functions.py @@ -2,10 +2,6 @@ import re import pytest -from bittensor.core.extrinsics.asyncex.utils import ( - get_extrinsic_fee as get_extrinsic_fee_async, -) -from bittensor.core.extrinsics.utils import get_extrinsic_fee from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging from tests.e2e_tests.utils import ( @@ -421,8 +417,8 @@ async def test_blocks_async(subtensor): (None, None, True), (1, None, True), (None, "SOME_HASH", True), - (1, "SOME_HASH", False) - ] + (1, "SOME_HASH", False), + ], ) def test_block_info(subtensor, block, block_hash, result): """Tests sync get_block_info.""" @@ -444,8 +440,8 @@ def test_block_info(subtensor, block, block_hash, result): (None, None, True), (1, None, True), (None, "SOME_HASH", True), - (1, "SOME_HASH", False) - ] + (1, "SOME_HASH", False), + ], ) @pytest.mark.asyncio async def test_block_info(async_subtensor, block, block_hash, result): @@ -456,7 +452,9 @@ async def test_block_info(async_subtensor, block, block_hash, result): await async_subtensor.wait_for_block(2) try: - res = await async_subtensor.chain.get_block_info(block=block, block_hash=block_hash) + res = await async_subtensor.chain.get_block_info( + block=block, block_hash=block_hash + ) assert (res is not None) == result except Exception as e: assert "Either block_hash or block_number should be set" in str(e) diff --git a/tests/e2e_tests/utils/__init__.py b/tests/e2e_tests/utils/__init__.py index fbfe8b4cdc..7e6ba3ec3b 100644 --- a/tests/e2e_tests/utils/__init__.py +++ b/tests/e2e_tests/utils/__init__.py @@ -73,7 +73,7 @@ def set_identity( additional="", ): return subtensor.sign_and_send_extrinsic( - call=subtensor.substrate.compose_call( + call=subtensor.compose_call( call_module="SubtensorModule", call_function="set_identity", call_params={ @@ -104,7 +104,7 @@ async def async_set_identity( additional="", ): return await subtensor.sign_and_send_extrinsic( - call=await subtensor.substrate.compose_call( + call=await subtensor.compose_call( call_module="SubtensorModule", call_function="set_identity", call_params={ @@ -125,7 +125,7 @@ async def async_set_identity( def propose(subtensor, wallet, proposal, duration): return subtensor.sign_and_send_extrinsic( - call=subtensor.substrate.compose_call( + call=subtensor.compose_call( call_module="Triumvirate", call_function="propose", call_params={ @@ -147,7 +147,7 @@ async def async_propose( duration, ): return await subtensor.sign_and_send_extrinsic( - call=await subtensor.substrate.compose_call( + call=await subtensor.compose_call( call_module="Triumvirate", call_function="propose", call_params={ @@ -171,7 +171,7 @@ def vote( approve, ): return subtensor.sign_and_send_extrinsic( - call=subtensor.substrate.compose_call( + call=subtensor.compose_call( call_module="SubtensorModule", call_function="vote", call_params={ @@ -196,7 +196,7 @@ async def async_vote( approve, ): return await subtensor.sign_and_send_extrinsic( - call=await subtensor.substrate.compose_call( + call=await subtensor.compose_call( call_module="SubtensorModule", call_function="vote", call_params={ diff --git a/tests/e2e_tests/utils/e2e_test_utils.py b/tests/e2e_tests/utils/e2e_test_utils.py index d004a26ace..76e0916bdf 100644 --- a/tests/e2e_tests/utils/e2e_test_utils.py +++ b/tests/e2e_tests/utils/e2e_test_utils.py @@ -3,7 +3,7 @@ import shutil import subprocess import sys - +from typing import Optional from bittensor_wallet import Keypair, Wallet from bittensor.extras import SubtensorApi @@ -15,15 +15,16 @@ def setup_wallet( uri: str, - encrypt_hotkey: bool = False, encrypt_coldkey: bool = False, + encrypt_hotkey: bool = False, + coldkey_password: Optional[str] = None, + hotkey_password: Optional[str] = None, ) -> tuple[Keypair, Wallet]: """ Sets up a wallet using the provided URI. - This function creates a keypair from the given URI and initializes a wallet - at a temporary path. It sets the coldkey, coldkeypub, and hotkey for the wallet - using the generated keypair. + This function creates a keypair from the given URI and initializes a wallet at a temporary path. It sets the + coldkey, coldkeypub, and hotkey for the wallet using the generated keypair. Side Effects: - Creates a wallet in a temporary directory. @@ -33,9 +34,20 @@ def setup_wallet( name = uri.strip("/") wallet_path = f"/tmp/btcli-e2e-wallet-{name}" wallet = Wallet(name=name, path=wallet_path) - wallet.set_coldkey(keypair=keypair, encrypt=encrypt_coldkey, overwrite=True) + wallet.set_coldkey( + keypair=keypair, + encrypt=encrypt_coldkey, + overwrite=True, + coldkey_password=coldkey_password, + ) wallet.set_coldkeypub(keypair=keypair, encrypt=False, overwrite=True) - wallet.set_hotkey(keypair=keypair, encrypt=encrypt_hotkey, overwrite=True) + wallet.set_hotkey( + keypair=keypair, + encrypt=encrypt_hotkey, + overwrite=True, + hotkey_password=hotkey_password, + ) + wallet.set_hotkeypub(keypair=keypair, encrypt=False, overwrite=True) return keypair, wallet diff --git a/tests/helpers/helpers.py b/tests/helpers/helpers.py index c4fa5b9151..71c7ddcfc2 100644 --- a/tests/helpers/helpers.py +++ b/tests/helpers/helpers.py @@ -87,7 +87,7 @@ def assert_submit_signed_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = True, ): - substrate.compose_call.assert_called_with( + substrate.assert_called_with( call_module, call_function, call_params, diff --git a/tests/integration_tests/test_config_does_not_process_cli_args.py b/tests/integration_tests/test_config_does_not_process_cli_args.py index 63c67a3224..0c7e846d81 100644 --- a/tests/integration_tests/test_config_does_not_process_cli_args.py +++ b/tests/integration_tests/test_config_does_not_process_cli_args.py @@ -22,9 +22,10 @@ def _config_call(): TEST_ARGS = [ "bittensor", - "--config", "path/to/config.yaml", + "--config", + "path/to/config.yaml", "--strict", - "--no_version_checking" + "--no_version_checking", ] diff --git a/tests/unit_tests/extrinsics/asyncex/test_children.py b/tests/unit_tests/extrinsics/asyncex/test_children.py index 8ce1fe9563..0d3ad36cad 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_children.py +++ b/tests/unit_tests/extrinsics/asyncex/test_children.py @@ -17,7 +17,7 @@ async def test_set_children_extrinsic(subtensor, mocker, fake_wallet): ), ] - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic", @@ -70,7 +70,7 @@ async def test_root_set_pending_childkey_cooldown_extrinsic( # Preps cooldown = 100 - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic", diff --git a/tests/unit_tests/extrinsics/asyncex/test_liquidity.py b/tests/unit_tests/extrinsics/asyncex/test_liquidity.py index 92cc03ff18..4f41cee97a 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_liquidity.py +++ b/tests/unit_tests/extrinsics/asyncex/test_liquidity.py @@ -6,16 +6,18 @@ async def test_add_liquidity_extrinsic(subtensor, fake_wallet, mocker): """Test that the add `add_liquidity_extrinsic` executes correct calls.""" # Preps - fake_netuid = 1 + fake_netuid = mocker.Mock() fake_liquidity = mocker.Mock() fake_price_low = mocker.Mock() fake_price_high = mocker.Mock() - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic" ) - mocked_price_to_tick = mocker.patch.object(liquidity, "price_to_tick") + mocked_param_add_liquidity = mocker.patch.object( + liquidity.LiquidityParams, "add_liquidity" + ) # Call result = await liquidity.add_liquidity_extrinsic( @@ -28,16 +30,17 @@ async def test_add_liquidity_extrinsic(subtensor, fake_wallet, mocker): ) # Asserts + mocked_param_add_liquidity.assert_called_once_with( + netuid=fake_netuid, + hotkey_ss58=fake_wallet.hotkey.ss58_address, + liquidity=fake_liquidity, + price_low=fake_price_low, + price_high=fake_price_high, + ) mocked_compose_call.assert_awaited_once_with( call_module="Swap", call_function="add_liquidity", - call_params={ - "hotkey": fake_wallet.hotkey.ss58_address, - "netuid": fake_netuid, - "tick_low": mocked_price_to_tick.return_value, - "tick_high": mocked_price_to_tick.return_value, - "liquidity": fake_liquidity.rao, - }, + call_params=mocked_param_add_liquidity.return_value, ) mocked_sign_and_send_extrinsic.assert_awaited_once_with( call=mocked_compose_call.return_value, @@ -58,7 +61,7 @@ async def test_modify_liquidity_extrinsic(subtensor, fake_wallet, mocker): fake_position_id = 2 fake_liquidity_delta = mocker.Mock() - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic" ) @@ -101,7 +104,7 @@ async def test_remove_liquidity_extrinsic(subtensor, fake_wallet, mocker): fake_netuid = 1 fake_position_id = 2 - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic" ) @@ -142,7 +145,7 @@ async def test_toggle_user_liquidity_extrinsic(subtensor, fake_wallet, mocker): fake_netuid = 1 fake_enable = mocker.Mock() - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic" ) diff --git a/tests/unit_tests/extrinsics/asyncex/test_registration.py b/tests/unit_tests/extrinsics/asyncex/test_registration.py index 047b653111..a9ba797d00 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_registration.py +++ b/tests/unit_tests/extrinsics/asyncex/test_registration.py @@ -26,7 +26,7 @@ async def test_register_extrinsic_success(subtensor, fake_wallet, mocker): is_stale_async=mocker.AsyncMock(return_value=False), seal=[] ), ) - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic", return_value=ExtrinsicResponse(True, "") ) @@ -92,7 +92,7 @@ async def test_register_extrinsic_success_with_cuda(subtensor, fake_wallet, mock is_stale_async=mocker.AsyncMock(return_value=False), seal=[] ), ) - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic", return_value=ExtrinsicResponse(True, "") ) @@ -263,7 +263,7 @@ async def is_stale_side_effect(*_, **__): "create_pow_async", return_value=fake_pow_result, ) - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic", @@ -318,7 +318,7 @@ async def test_set_subnet_identity_extrinsic_is_success(subtensor, fake_wallet, description = "mock_description" additional = "mock_additional" - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic" @@ -383,7 +383,7 @@ async def test_set_subnet_identity_extrinsic_is_failed(subtensor, fake_wallet, m additional = "mock_additional" fake_error_message = "error message" - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, diff --git a/tests/unit_tests/extrinsics/asyncex/test_root.py b/tests/unit_tests/extrinsics/asyncex/test_root.py index cc0b7112aa..aa3b4a0de8 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_root.py +++ b/tests/unit_tests/extrinsics/asyncex/test_root.py @@ -52,7 +52,7 @@ async def test_root_register_extrinsic_success(subtensor, fake_wallet, mocker): "is_hotkey_registered", return_value=False, ) - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic", @@ -83,7 +83,7 @@ async def test_root_register_extrinsic_success(subtensor, fake_wallet, mocker): ) # Asserts - mocked_unlock_key.assert_called_once_with(fake_wallet, False) + mocked_unlock_key.assert_called_once_with(fake_wallet, False, unlock_type="both") mocked_is_hotkey_registered.assert_called_once_with( netuid=0, hotkey_ss58="fake_hotkey_address" ) @@ -129,7 +129,7 @@ async def test_root_register_extrinsic_insufficient_balance( wait_for_finalization=True, ) - mocked_unlock_key.assert_called_once_with(fake_wallet, False) + mocked_unlock_key.assert_called_once_with(fake_wallet, False, unlock_type="both") assert result.success is False subtensor.get_balance.assert_called_once_with( @@ -168,7 +168,7 @@ async def test_root_register_extrinsic_unlock_failed(subtensor, fake_wallet, moc ) # Asserts - mocked_unlock_key.assert_called_once_with(fake_wallet, False) + mocked_unlock_key.assert_called_once_with(fake_wallet, False, unlock_type="both") assert result.success is False @@ -210,7 +210,7 @@ async def test_root_register_extrinsic_already_registered( ) # Asserts - mocked_unlock_key.assert_called_once_with(fake_wallet, False) + mocked_unlock_key.assert_called_once_with(fake_wallet, False, unlock_type="both") mocked_is_hotkey_registered.assert_called_once_with( netuid=0, hotkey_ss58="fake_hotkey_address" ) @@ -245,7 +245,7 @@ async def test_root_register_extrinsic_transaction_failed( "is_hotkey_registered", return_value=False, ) - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic", @@ -261,7 +261,7 @@ async def test_root_register_extrinsic_transaction_failed( ) # Asserts - mocked_unlock_key.assert_called_once_with(fake_wallet, False) + mocked_unlock_key.assert_called_once_with(fake_wallet, False, unlock_type="both") mocked_is_hotkey_registered.assert_called_once_with( netuid=0, hotkey_ss58="fake_hotkey_address" ) @@ -296,7 +296,7 @@ async def test_root_register_extrinsic_uid_not_found(subtensor, fake_wallet, moc "is_hotkey_registered", return_value=False, ) - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic", @@ -317,7 +317,7 @@ async def test_root_register_extrinsic_uid_not_found(subtensor, fake_wallet, moc ) # Asserts - mocked_unlock_key.assert_called_once_with(fake_wallet, False) + mocked_unlock_key.assert_called_once_with(fake_wallet, False, unlock_type="both") mocked_is_hotkey_registered.assert_called_once_with( netuid=0, hotkey_ss58="fake_hotkey_address" ) diff --git a/tests/unit_tests/extrinsics/asyncex/test_staking.py b/tests/unit_tests/extrinsics/asyncex/test_staking.py index 497362094f..6d1571fcf7 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_staking.py +++ b/tests/unit_tests/extrinsics/asyncex/test_staking.py @@ -20,7 +20,7 @@ async def test_set_auto_stake_extrinsic( netuid = mocker.Mock() hotkey_ss58 = mocker.Mock() - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, diff --git a/tests/unit_tests/extrinsics/asyncex/test_start_call.py b/tests/unit_tests/extrinsics/asyncex/test_start_call.py index 23d9bf755c..1f23111544 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_start_call.py +++ b/tests/unit_tests/extrinsics/asyncex/test_start_call.py @@ -10,8 +10,7 @@ async def test_start_call_extrinsics(subtensor, mocker, fake_wallet): wallet = fake_wallet wallet.name = "fake_wallet" wallet.coldkey = "fake_coldkey" - substrate = subtensor.substrate.__aenter__.return_value - substrate.compose_call = mocker.AsyncMock() + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic", return_value=(True, "Success") ) @@ -24,14 +23,14 @@ async def test_start_call_extrinsics(subtensor, mocker, fake_wallet): ) # Assertions - substrate.compose_call.assert_awaited_once_with( + mocked_compose_call.assert_awaited_once_with( call_module="SubtensorModule", call_function="start_call", call_params={"netuid": netuid}, ) mocked_sign_and_send_extrinsic.assert_awaited_once_with( - call=substrate.compose_call.return_value, + call=mocked_compose_call.return_value, wallet=wallet, wait_for_inclusion=True, wait_for_finalization=False, diff --git a/tests/unit_tests/extrinsics/asyncex/test_transfer.py b/tests/unit_tests/extrinsics/asyncex/test_transfer.py index 4fe074e8b7..c27880ece0 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_transfer.py +++ b/tests/unit_tests/extrinsics/asyncex/test_transfer.py @@ -37,7 +37,7 @@ async def test_transfer_extrinsic_success(subtensor, fake_wallet, mocker): subtensor.get_transfer_fee = mocker.patch.object( subtensor, "get_transfer_fee", return_value=2 ) - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic", return_value=ExtrinsicResponse(True, "") ) @@ -109,7 +109,7 @@ async def test_transfer_extrinsic_call_successful_with_failed_response( subtensor.get_transfer_fee = mocker.patch.object( subtensor, "get_transfer_fee", return_value=2 ) - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic", return_value=ExtrinsicResponse(False, "") ) @@ -300,7 +300,7 @@ async def test_transfer_extrinsic_keep_alive_false_and_transfer_all_true( subtensor.get_transfer_fee = mocker.patch.object( subtensor, "get_transfer_fee", return_value=2 ) - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic", return_value=ExtrinsicResponse(True, "") ) diff --git a/tests/unit_tests/extrinsics/asyncex/test_unstaking.py b/tests/unit_tests/extrinsics/asyncex/test_unstaking.py index 4a68883f29..6d4efe2360 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_unstaking.py +++ b/tests/unit_tests/extrinsics/asyncex/test_unstaking.py @@ -24,6 +24,8 @@ async def test_unstake_extrinsic(fake_wallet, mocker): } ) + mocked_params = mocker.patch.object(unstaking.UnstakingParams, "remove_stake") + fake_wallet.coldkeypub.ss58_address = "hotkey_owner" hotkey_ss58 = "hotkey" amount = Balance.from_tao(1.1) @@ -44,17 +46,13 @@ async def test_unstake_extrinsic(fake_wallet, mocker): # Asserts assert result.success is True - fake_subtensor.substrate.compose_call.assert_awaited_once_with( + fake_subtensor.compose_call.assert_awaited_once_with( call_module="SubtensorModule", call_function="remove_stake", - call_params={ - "hotkey": "hotkey", - "amount_unstaked": 1100000000, - "netuid": fake_netuid, - }, + call_params=mocked_params.return_value, ) fake_subtensor.sign_and_send_extrinsic.assert_awaited_once_with( - call=fake_subtensor.substrate.compose_call.return_value, + call=fake_subtensor.compose_call.return_value, wallet=fake_wallet, wait_for_inclusion=True, wait_for_finalization=True, @@ -74,7 +72,7 @@ async def test_unstake_all_extrinsic(fake_wallet, mocker): "sign_and_send_extrinsic.return_value": (True, ""), } ) - fake_substrate = fake_subtensor.substrate.__aenter__.return_value + mocked_compose_call = mocker.patch.object(fake_subtensor, "compose_call") hotkey = "hotkey" fake_netuid = 1 @@ -90,7 +88,7 @@ async def test_unstake_all_extrinsic(fake_wallet, mocker): assert result[0] is True assert result[1] == "" - fake_substrate.compose_call.assert_awaited_once_with( + mocked_compose_call.assert_awaited_once_with( call_module="SubtensorModule", call_function="remove_stake_full_limit", call_params={ @@ -100,7 +98,7 @@ async def test_unstake_all_extrinsic(fake_wallet, mocker): }, ) fake_subtensor.sign_and_send_extrinsic.assert_awaited_once_with( - call=fake_substrate.compose_call.return_value, + call=mocked_compose_call.return_value, wallet=fake_wallet, wait_for_inclusion=True, wait_for_finalization=True, diff --git a/tests/unit_tests/extrinsics/asyncex/test_weights.py b/tests/unit_tests/extrinsics/asyncex/test_weights.py index 5b6260a580..649dc79b15 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_weights.py +++ b/tests/unit_tests/extrinsics/asyncex/test_weights.py @@ -23,7 +23,7 @@ async def test_commit_weights_extrinsic(mocker, subtensor, fake_wallet): mocked_generate_weight_hash = mocker.patch.object( weights_module, "generate_weight_hash" ) - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic", return_value=ExtrinsicResponse(True, "") ) @@ -103,7 +103,7 @@ async def test_commit_timelocked_weights_extrinsic(mocker, subtensor, fake_walle "get_encrypted_commit", return_value=(mocker.Mock(), mocker.Mock()), ) - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic", @@ -182,7 +182,7 @@ async def test_reveal_weights_extrinsic(mocker, subtensor, fake_wallet): "convert_and_normalize_weights_and_uids", return_value=(uids, weights), ) - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic", return_value=ExtrinsicResponse(True, "") ) @@ -243,7 +243,7 @@ async def test_set_weights_extrinsic(mocker, subtensor, fake_wallet): "convert_and_normalize_weights_and_uids", return_value=(uids, weights), ) - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic", diff --git a/tests/unit_tests/extrinsics/test_children.py b/tests/unit_tests/extrinsics/test_children.py index ca5b53b0d8..8ec0dc2f68 100644 --- a/tests/unit_tests/extrinsics/test_children.py +++ b/tests/unit_tests/extrinsics/test_children.py @@ -14,7 +14,7 @@ def test_set_children_extrinsic(subtensor, mocker, fake_wallet): ), ] - subtensor.substrate.compose_call = mocker.Mock() + subtensor.compose_call = mocker.Mock() mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic", @@ -31,7 +31,7 @@ def test_set_children_extrinsic(subtensor, mocker, fake_wallet): ) # Asserts - subtensor.substrate.compose_call.assert_called_once_with( + subtensor.compose_call.assert_called_once_with( call_module="SubtensorModule", call_function="set_children", call_params={ @@ -47,7 +47,7 @@ def test_set_children_extrinsic(subtensor, mocker, fake_wallet): ) mocked_sign_and_send_extrinsic.assert_called_once_with( - call=subtensor.substrate.compose_call.return_value, + call=subtensor.compose_call.return_value, wallet=fake_wallet, period=None, raise_error=False, @@ -64,7 +64,7 @@ def test_root_set_pending_childkey_cooldown_extrinsic(subtensor, mocker, fake_wa # Preps cooldown = 100 - subtensor.substrate.compose_call = mocker.Mock() + subtensor.compose_call = mocker.Mock() mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic", @@ -79,9 +79,8 @@ def test_root_set_pending_childkey_cooldown_extrinsic(subtensor, mocker, fake_wa ) # Asserts - subtensor.substrate.compose_call.call_count == 2 mocked_sign_and_send_extrinsic.assert_called_once_with( - call=subtensor.substrate.compose_call.return_value, + call=subtensor.compose_call.return_value, wallet=fake_wallet, nonce_key="hotkey", sign_with="coldkey", diff --git a/tests/unit_tests/extrinsics/test_liquidity.py b/tests/unit_tests/extrinsics/test_liquidity.py index 26ab1686d1..0281ae6235 100644 --- a/tests/unit_tests/extrinsics/test_liquidity.py +++ b/tests/unit_tests/extrinsics/test_liquidity.py @@ -9,11 +9,11 @@ def test_add_liquidity_extrinsic(subtensor, fake_wallet, mocker): fake_price_low = mocker.Mock() fake_price_high = mocker.Mock() - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic" ) - mocked_price_to_tick = mocker.patch.object(liquidity, "price_to_tick") + mocked_params = mocker.patch.object(liquidity.LiquidityParams, "add_liquidity") # Call result = liquidity.add_liquidity_extrinsic( @@ -29,13 +29,7 @@ def test_add_liquidity_extrinsic(subtensor, fake_wallet, mocker): mocked_compose_call.assert_called_once_with( call_module="Swap", call_function="add_liquidity", - call_params={ - "hotkey": fake_wallet.hotkey.ss58_address, - "netuid": fake_netuid, - "tick_low": mocked_price_to_tick.return_value, - "tick_high": mocked_price_to_tick.return_value, - "liquidity": fake_liquidity.rao, - }, + call_params=mocked_params.return_value, ) mocked_sign_and_send_extrinsic.assert_called_once_with( call=mocked_compose_call.return_value, @@ -55,7 +49,7 @@ def test_modify_liquidity_extrinsic(subtensor, fake_wallet, mocker): fake_position_id = 2 fake_liquidity_delta = mocker.Mock() - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic" ) @@ -97,7 +91,7 @@ def test_remove_liquidity_extrinsic(subtensor, fake_wallet, mocker): fake_netuid = 1 fake_position_id = 2 - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic" ) @@ -137,7 +131,7 @@ def test_toggle_user_liquidity_extrinsic(subtensor, fake_wallet, mocker): fake_netuid = 1 fake_enable = mocker.Mock() - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic" ) diff --git a/tests/unit_tests/extrinsics/test_registration.py b/tests/unit_tests/extrinsics/test_registration.py index d14685e1fa..abf65beb24 100644 --- a/tests/unit_tests/extrinsics/test_registration.py +++ b/tests/unit_tests/extrinsics/test_registration.py @@ -254,7 +254,7 @@ def test_set_subnet_identity_extrinsic_is_success(mock_subtensor, mock_wallet, m description = "mock_description" additional = "mock_additional" - mocked_compose_call = mocker.patch.object(mock_subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(mock_subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( mock_subtensor, "sign_and_send_extrinsic" ) @@ -318,7 +318,7 @@ def test_set_subnet_identity_extrinsic_is_failed(mock_subtensor, mock_wallet, mo fake_error_message = "error message" - mocked_compose_call = mocker.patch.object(mock_subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(mock_subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( mock_subtensor, "sign_and_send_extrinsic", diff --git a/tests/unit_tests/extrinsics/test_root.py b/tests/unit_tests/extrinsics/test_root.py index 7d887964bf..bd40eae28a 100644 --- a/tests/unit_tests/extrinsics/test_root.py +++ b/tests/unit_tests/extrinsics/test_root.py @@ -101,13 +101,13 @@ def test_root_register_extrinsic( assert result.success == expected_result if not hotkey_registered[0]: - mock_subtensor.substrate.compose_call.assert_called_once_with( + mock_subtensor.compose_call.assert_called_once_with( call_module="SubtensorModule", call_function="root_register", call_params={"hotkey": "fake_hotkey_address"}, ) mocked_sign_and_send_extrinsic.assert_called_once_with( - call=mock_subtensor.substrate.compose_call.return_value, + call=mock_subtensor.compose_call.return_value, wallet=mock_wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, diff --git a/tests/unit_tests/extrinsics/test_serving.py b/tests/unit_tests/extrinsics/test_serving.py index 619ca2e565..427609cf85 100644 --- a/tests/unit_tests/extrinsics/test_serving.py +++ b/tests/unit_tests/extrinsics/test_serving.py @@ -351,7 +351,7 @@ def test_publish_metadata( ): # Arrange with ( - patch.object(mock_subtensor.substrate, "compose_call"), + patch.object(mock_subtensor, "compose_call"), patch.object( mock_subtensor, "sign_and_send_extrinsic", return_value=response_success ) as mocked_sign_and_send_extrinsic, @@ -369,7 +369,7 @@ def test_publish_metadata( # Assert assert result.success is True, f"Test ID: {test_id}" mocked_sign_and_send_extrinsic.assert_called_once_with( - call=mock_subtensor.substrate.compose_call.return_value, + call=mock_subtensor.compose_call.return_value, wallet=mock_wallet, sign_with="hotkey", wait_for_inclusion=wait_for_inclusion, diff --git a/tests/unit_tests/extrinsics/test_staking.py b/tests/unit_tests/extrinsics/test_staking.py index d0d9c5276c..8696677320 100644 --- a/tests/unit_tests/extrinsics/test_staking.py +++ b/tests/unit_tests/extrinsics/test_staking.py @@ -8,12 +8,15 @@ def test_add_stake_extrinsic(mocker): """Verify that sync `add_stake_extrinsic` method calls proper async method.""" # Preps + fake_extrinsic_fee = Balance.from_tao(0.1) fake_subtensor = mocker.Mock( **{ "get_balance.return_value": Balance(10), "get_existential_deposit.return_value": Balance(1), "get_hotkey_owner.return_value": "hotkey_owner", - "sign_and_send_extrinsic.return_value": ExtrinsicResponse(True, "Success"), + "sign_and_send_extrinsic.return_value": ExtrinsicResponse( + True, "Success", extrinsic_fee=fake_extrinsic_fee + ), } ) fake_wallet_ = mocker.Mock( @@ -40,14 +43,15 @@ def test_add_stake_extrinsic(mocker): # Asserts assert result.success is True + assert result.extrinsic_fee == fake_extrinsic_fee - fake_subtensor.substrate.compose_call.assert_called_once_with( + fake_subtensor.compose_call.assert_called_once_with( call_module="SubtensorModule", call_function="add_stake", call_params={"hotkey": "hotkey", "amount_staked": 9, "netuid": 1}, ) fake_subtensor.sign_and_send_extrinsic.assert_called_once_with( - call=fake_subtensor.substrate.compose_call.return_value, + call=fake_subtensor.compose_call.return_value, wallet=fake_wallet_, wait_for_inclusion=True, wait_for_finalization=True, @@ -119,7 +123,7 @@ def test_set_auto_stake_extrinsic( netuid = mocker.Mock() hotkey_ss58 = mocker.Mock() - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, diff --git a/tests/unit_tests/extrinsics/test_start_call.py b/tests/unit_tests/extrinsics/test_start_call.py index 7f85675620..8cd36c4ffd 100644 --- a/tests/unit_tests/extrinsics/test_start_call.py +++ b/tests/unit_tests/extrinsics/test_start_call.py @@ -9,7 +9,7 @@ def test_start_call_extrinsics(subtensor, mocker, fake_wallet): wallet.name = "fake_wallet" wallet.coldkey = "fake_coldkey" - subtensor.substrate.compose_call = mocker.Mock() + subtensor.compose_call = mocker.Mock() mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic", return_value=(True, "Success") ) @@ -22,14 +22,14 @@ def test_start_call_extrinsics(subtensor, mocker, fake_wallet): ) # Assertions - subtensor.substrate.compose_call.assert_called_once_with( + subtensor.compose_call.assert_called_once_with( call_module="SubtensorModule", call_function="start_call", call_params={"netuid": netuid}, ) mocked_sign_and_send_extrinsic.assert_called_once_with( - call=subtensor.substrate.compose_call.return_value, + call=subtensor.compose_call.return_value, wallet=wallet, wait_for_inclusion=True, wait_for_finalization=False, diff --git a/tests/unit_tests/extrinsics/test_transfer.py b/tests/unit_tests/extrinsics/test_transfer.py index e8bce586ed..c6bdeca975 100644 --- a/tests/unit_tests/extrinsics/test_transfer.py +++ b/tests/unit_tests/extrinsics/test_transfer.py @@ -35,7 +35,7 @@ def test_transfer_extrinsic_success(subtensor, fake_wallet, mocker): subtensor.get_transfer_fee = mocker.patch.object( subtensor, "get_transfer_fee", return_value=2 ) - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic", return_value=ExtrinsicResponse(True, "") ) @@ -106,7 +106,7 @@ def test_transfer_extrinsic_call_successful_with_failed_response( subtensor.get_transfer_fee = mocker.patch.object( subtensor, "get_transfer_fee", return_value=2 ) - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic", return_value=ExtrinsicResponse(False, "") ) @@ -298,7 +298,7 @@ def test_transfer_extrinsic_keep_alive_false_and_transfer_all_true( subtensor.get_transfer_fee = mocker.patch.object( subtensor, "get_transfer_fee", return_value=2 ) - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic", return_value=ExtrinsicResponse(True, "") ) diff --git a/tests/unit_tests/extrinsics/test_unstaking.py b/tests/unit_tests/extrinsics/test_unstaking.py index 807ee472eb..92f1192954 100644 --- a/tests/unit_tests/extrinsics/test_unstaking.py +++ b/tests/unit_tests/extrinsics/test_unstaking.py @@ -40,7 +40,7 @@ def test_unstake_extrinsic(fake_wallet, mocker): # Asserts assert result.success is True - fake_subtensor.substrate.compose_call.assert_called_once_with( + fake_subtensor.compose_call.assert_called_once_with( call_module="SubtensorModule", call_function="remove_stake", call_params={ @@ -50,7 +50,7 @@ def test_unstake_extrinsic(fake_wallet, mocker): }, ) fake_subtensor.sign_and_send_extrinsic.assert_called_once_with( - call=fake_subtensor.substrate.compose_call.return_value, + call=fake_subtensor.compose_call.return_value, wallet=fake_wallet, wait_for_inclusion=True, wait_for_finalization=True, @@ -85,7 +85,7 @@ def test_unstake_all_extrinsic(fake_wallet, mocker): assert result[0] is True assert result[1] == "" - fake_subtensor.substrate.compose_call.assert_called_once_with( + fake_subtensor.compose_call.assert_called_once_with( call_module="SubtensorModule", call_function="remove_stake_full_limit", call_params={ @@ -95,7 +95,7 @@ def test_unstake_all_extrinsic(fake_wallet, mocker): }, ) fake_subtensor.sign_and_send_extrinsic.assert_called_once_with( - call=fake_subtensor.substrate.compose_call.return_value, + call=fake_subtensor.compose_call.return_value, wallet=fake_wallet, wait_for_inclusion=True, wait_for_finalization=True, diff --git a/tests/unit_tests/extrinsics/test_weights.py b/tests/unit_tests/extrinsics/test_weights.py index a052d69bb5..99c0b233b3 100644 --- a/tests/unit_tests/extrinsics/test_weights.py +++ b/tests/unit_tests/extrinsics/test_weights.py @@ -30,7 +30,7 @@ def test_commit_timelocked_weights_extrinsic(mocker, subtensor, fake_wallet): "get_encrypted_commit", return_value=(mocker.Mock(), mocker.Mock()), ) - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic", @@ -109,7 +109,7 @@ def test_commit_weights_extrinsic(mocker, subtensor, fake_wallet): mocked_generate_weight_hash = mocker.patch.object( weights_module, "generate_weight_hash" ) - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic", return_value=ExtrinsicResponse(True, "") ) @@ -176,7 +176,7 @@ def test_reveal_weights_extrinsic(mocker, subtensor, fake_wallet): "convert_and_normalize_weights_and_uids", return_value=(uids, weights), ) - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic", return_value=ExtrinsicResponse(True, "") ) @@ -236,7 +236,7 @@ def test_set_weights_extrinsic(mocker, subtensor, fake_wallet): "convert_and_normalize_weights_and_uids", return_value=(uids, weights), ) - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") mocked_sign_and_send_extrinsic = mocker.patch.object( subtensor, "sign_and_send_extrinsic", diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 37d02cf381..7961a59780 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -19,7 +19,6 @@ from bittensor.core.types import ExtrinsicResponse from bittensor.utils import U64_MAX, get_function_name from bittensor.utils.balance import Balance -from tests.helpers.helpers import assert_submit_signed_extrinsic @pytest.fixture @@ -166,47 +165,60 @@ async def test_async_subtensor_aenter_connection_refused_error( @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)(), +async def test_burned_register(subtensor, fake_wallet, mocker): + # Preps + mocked_compose_call = mocker.patch.object(subtensor, "compose_call") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic", return_value=ExtrinsicResponse(True, "") ) - mock_substrate.get_payment_info.return_value = {"partial_fee": 10} - mocker.patch.object( + mocked_get_neuron_for_pubkey_and_subnet = mocker.patch.object( subtensor, "get_neuron_for_pubkey_and_subnet", return_value=NeuronInfo.get_null_neuron(), ) - mocker.patch.object( + mocked_get_balance = mocker.patch.object( subtensor, "get_balance", - return_value=Balance(1), + return_value=Balance.from_tao(1), ) + mocked_recycle = mocker.patch.object(subtensor, "recycle") + fake_netuid = 14 + # Call success, _ = await subtensor.burned_register( - fake_wallet, - netuid=1, + wallet=fake_wallet, + netuid=fake_netuid, ) + # Asserts 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, + mocked_compose_call.assert_awaited_once_with( call_module="SubtensorModule", call_function="burned_register", call_params={ - "netuid": 1, "hotkey": fake_wallet.hotkey.ss58_address, + "netuid": fake_netuid, }, + ) + mocked_sign_and_send_extrinsic.assert_awaited_once_with( + call=mocked_compose_call.return_value, + wallet=fake_wallet, + period=None, + raise_error=False, wait_for_finalization=True, wait_for_inclusion=True, ) + mocked_get_neuron_for_pubkey_and_subnet.assert_awaited_once_with( + hotkey_ss58=fake_wallet.hotkey.ss58_address, + block_hash=subtensor.substrate.get_chain_head.return_value, + netuid=fake_netuid, + ) + mocked_get_balance.assert_awaited_with(address=fake_wallet.coldkeypub.ss58_address) + mocked_recycle.assert_awaited_with( + netuid=fake_netuid, + block_hash=subtensor.substrate.get_chain_head.return_value, + ) @pytest.mark.asyncio @@ -673,7 +685,7 @@ async def test_get_transfer_fee(subtensor, fake_wallet, mocker, balance): fake_value = balance mocked_compose_call = mocker.AsyncMock() - subtensor.substrate.compose_call = mocked_compose_call + subtensor.compose_call = mocked_compose_call mocked_get_payment_info = mocker.AsyncMock(return_value={"partial_fee": 100}) subtensor.substrate.get_payment_info = mocked_get_payment_info @@ -708,7 +720,7 @@ async def test_get_transfer_with_exception(subtensor, mocker): fake_value = 123 mocked_compose_call = mocker.AsyncMock() - subtensor.substrate.compose_call = mocked_compose_call + subtensor.compose_call = mocked_compose_call subtensor.substrate.get_payment_info.side_effect = Exception # Call + Assertions @@ -1681,7 +1693,7 @@ async def test_sign_and_send_extrinsic_success_finalization( fake_extrinsic = mocker.Mock() fake_response = mocker.Mock() - mocked_get_extrinsic_fee = mocker.patch.object(async_subtensor, "get_extrinsic_fee") + mocked_get_extrinsic_fee = mocker.patch.object(subtensor, "get_extrinsic_fee") mocked_create_signed_extrinsic = mocker.AsyncMock(return_value=fake_extrinsic) subtensor.substrate.create_signed_extrinsic = mocked_create_signed_extrinsic @@ -1706,7 +1718,7 @@ async def fake_is_success(): # Asserts mocked_get_extrinsic_fee.assert_awaited_once_with( - subtensor=subtensor, call=fake_call, keypair=fake_wallet.coldkey + call=fake_call, keypair=fake_wallet.coldkey ) mocked_create_signed_extrinsic.assert_called_once_with( call=fake_call, keypair=fake_wallet.coldkey @@ -1735,7 +1747,7 @@ async def test_sign_and_send_extrinsic_error_finalization( fake_response = mocker.Mock() fake_error = {"some error": "message"} - mocked_get_extrinsic_fee = mocker.patch.object(async_subtensor, "get_extrinsic_fee") + mocked_get_extrinsic_fee = mocker.patch.object(subtensor, "get_extrinsic_fee") mocked_create_signed_extrinsic = mocker.AsyncMock(return_value=fake_extrinsic) subtensor.substrate.create_signed_extrinsic = mocked_create_signed_extrinsic @@ -1768,7 +1780,7 @@ async def fake_error_message(): # Asserts mocked_get_extrinsic_fee.assert_awaited_once_with( - subtensor=subtensor, call=fake_call, keypair=fake_wallet.coldkey + call=fake_call, keypair=fake_wallet.coldkey ) mocked_create_signed_extrinsic.assert_called_once_with( call=fake_call, keypair=fake_wallet.coldkey @@ -1795,7 +1807,7 @@ async def test_sign_and_send_extrinsic_success_without_inclusion_finalization( fake_call = mocker.Mock() fake_extrinsic = mocker.Mock() - mocked_get_extrinsic_fee = mocker.patch.object(async_subtensor, "get_extrinsic_fee") + mocked_get_extrinsic_fee = mocker.patch.object(subtensor, "get_extrinsic_fee") mocked_create_signed_extrinsic = mocker.AsyncMock(return_value=fake_extrinsic) subtensor.substrate.create_signed_extrinsic = mocked_create_signed_extrinsic @@ -1813,7 +1825,7 @@ async def test_sign_and_send_extrinsic_success_without_inclusion_finalization( # Asserts mocked_get_extrinsic_fee.assert_awaited_once_with( - subtensor=subtensor, call=fake_call, keypair=fake_wallet.coldkey + call=fake_call, keypair=fake_wallet.coldkey ) mocked_create_signed_extrinsic.assert_awaited_once() mocked_create_signed_extrinsic.assert_called_once_with( @@ -1843,7 +1855,7 @@ async def test_sign_and_send_extrinsic_substrate_request_exception( fake_extrinsic = mocker.Mock() fake_exception = async_subtensor.SubstrateRequestException("Test Exception") - mocked_get_extrinsic_fee = mocker.patch.object(async_subtensor, "get_extrinsic_fee") + mocked_get_extrinsic_fee = mocker.patch.object(subtensor, "get_extrinsic_fee") mocked_create_signed_extrinsic = mocker.AsyncMock(return_value=fake_extrinsic) subtensor.substrate.create_signed_extrinsic = mocked_create_signed_extrinsic @@ -1867,7 +1879,7 @@ async def test_sign_and_send_extrinsic_substrate_request_exception( # Asserts mocked_get_extrinsic_fee.assert_awaited_once_with( - subtensor=subtensor, call=fake_call, keypair=fake_wallet.coldkey + call=fake_call, keypair=fake_wallet.coldkey ) assert result == (False, str(fake_exception)) assert result.extrinsic_function == get_function_name() @@ -1883,7 +1895,7 @@ async def test_sign_and_send_extrinsic_raises_error( ): """Tests sign_and_send_extrinsic when an error is raised.""" # Preps - mocked_get_extrinsic_fee = mocker.patch.object(async_subtensor, "get_extrinsic_fee") + mocked_get_extrinsic_fee = mocker.patch.object(subtensor, "get_extrinsic_fee") mock_substrate.submit_extrinsic.return_value = mocker.AsyncMock( error_message=mocker.AsyncMock( @@ -3778,42 +3790,13 @@ async def test_subnet(subtensor, mocker): assert result == mocked_di_from_dict.return_value -@pytest.mark.asyncio -async def test_get_stake_operations_fee(subtensor, mocker): - """Verify that `get_stake_operations_fee` calls proper methods and returns the correct value.""" - # Preps - netuid = 1 - amount = Balance.from_rao(100_000_000_000) # 100 Tao - mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") - mocked_query_map = mocker.patch.object( - subtensor.substrate, "query", return_value=mocker.Mock(value=196) - ) - - # Call - result = await subtensor.get_stake_operations_fee(netuid=netuid, amount=amount) - - # Assert - mocked_determine_block_hash.assert_awaited_once_with( - block=None, block_hash=None, reuse_block=False - ) - mocked_query_map.assert_awaited_once_with( - module="Swap", - storage_function="FeeRate", - params=[netuid], - block_hash=mocked_determine_block_hash.return_value, - ) - assert result == Balance.from_rao(299076829) - - @pytest.mark.asyncio async def test_get_stake_add_fee(subtensor, mocker): """Verify that `get_stake_add_fee` calls proper methods and returns the correct value.""" # Preps netuid = mocker.Mock() amount = mocker.Mock(spec=Balance) - mocked_get_stake_operations_fee = mocker.patch.object( - subtensor, "get_stake_operations_fee" - ) + mocked_sim_swap = mocker.patch.object(subtensor, "sim_swap") # Call result = await subtensor.get_stake_add_fee( @@ -3822,10 +3805,13 @@ async def test_get_stake_add_fee(subtensor, mocker): ) # Asserts - mocked_get_stake_operations_fee.assert_awaited_once_with( - amount=amount, netuid=netuid, block=None + mocked_sim_swap.assert_awaited_once_with( + origin_netuid=0, + destination_netuid=netuid, + amount=amount, + block_hash=None, ) - assert result == mocked_get_stake_operations_fee.return_value + assert result == mocked_sim_swap.return_value.tao_fee @pytest.mark.asyncio @@ -3834,8 +3820,11 @@ async def test_get_unstake_fee(subtensor, mocker): # Preps netuid = mocker.Mock() amount = mocker.Mock(spec=Balance) - mocked_get_stake_operations_fee = mocker.patch.object( - subtensor, "get_stake_operations_fee" + mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + mocked_sim_swap = mocker.patch.object( + subtensor, + "sim_swap", + return_value=mocker.MagicMock(alpha_fee=mocker.MagicMock()), ) # Call @@ -3845,33 +3834,45 @@ async def test_get_unstake_fee(subtensor, mocker): ) # Asserts - mocked_get_stake_operations_fee.assert_awaited_once_with( - amount=amount, netuid=netuid, block=None + mocked_sim_swap.assert_awaited_once_with( + origin_netuid=netuid, + destination_netuid=0, + amount=amount, + block_hash=mocked_determine_block_hash.return_value, ) - assert result == mocked_get_stake_operations_fee.return_value + assert result == mocked_sim_swap.return_value.alpha_fee.set_unit.return_value @pytest.mark.asyncio async def test_get_stake_movement_fee(subtensor, mocker): """Verify that `get_stake_movement_fee` calls proper methods and returns the correct value.""" # Preps - netuid = mocker.Mock() + origin_netuid = mocker.Mock() + destination_netuid = mocker.Mock() amount = mocker.Mock(spec=Balance) - mocked_get_stake_operations_fee = mocker.patch.object( - subtensor, "get_stake_operations_fee" + + mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + mocked_sim_swap = mocker.patch.object( + subtensor, + "sim_swap", + return_value=mocker.MagicMock(alpha_fee=mocker.MagicMock()), ) # Call result = await subtensor.get_stake_movement_fee( + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, amount=amount, - origin_netuid=netuid, ) # Asserts - mocked_get_stake_operations_fee.assert_awaited_once_with( - amount=amount, netuid=netuid, block=None + mocked_sim_swap.assert_awaited_once_with( + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + amount=amount, + block_hash=mocked_determine_block_hash.return_value, ) - assert result == mocked_get_stake_operations_fee.return_value + assert result == mocked_sim_swap.return_value.tao_fee @pytest.mark.asyncio @@ -4196,10 +4197,11 @@ async def test_get_block_info(subtensor, mocker): }, "extrinsics": [ fake_decoded, - ] - + ], } - mocked_get_block = mocker.patch.object(subtensor.substrate, "get_block", return_value=fake_substrate_block) + mocked_get_block = mocker.patch.object( + subtensor.substrate, "get_block", return_value=fake_substrate_block + ) mocked_BlockInfo = mocker.patch.object(async_subtensor, "BlockInfo") # Call @@ -4217,6 +4219,6 @@ async def test_get_block_info(subtensor, mocker): timestamp=fake_timestamp, header=fake_substrate_block.get("header"), extrinsics=fake_substrate_block.get("extrinsics"), - explorer=f"{settings.TAO_APP_BLOCK_EXPLORER}{fake_block}" + explorer=f"{settings.TAO_APP_BLOCK_EXPLORER}{fake_block}", ) assert result == mocked_BlockInfo.return_value diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index ab35d422bb..0333929019 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -1652,9 +1652,7 @@ def test_get_last_commitment_bonds_reset_block(subtensor, mocker): fake_uid = 2 fake_hotkey = "hotkey" - mocked_get_last_bonds_reset = mocker.patch.object( - subtensor, "get_last_bonds_reset" - ) + mocked_get_last_bonds_reset = mocker.patch.object(subtensor, "get_last_bonds_reset") mocked_decode_block = mocker.patch.object(subtensor_module, "decode_block") mocked_metagraph = mocker.MagicMock() @@ -1667,13 +1665,11 @@ def test_get_last_commitment_bonds_reset_block(subtensor, mocker): ) # Assertions - mocked_metagraph.assert_called_once_with( - fake_netuid, - block=None) - mocked_get_last_bonds_reset.assert_called_once_with( - fake_netuid, fake_hotkey, None + mocked_metagraph.assert_called_once_with(fake_netuid, block=None) + mocked_get_last_bonds_reset.assert_called_once_with(fake_netuid, fake_hotkey, None) + mocked_decode_block.assert_called_once_with( + mocked_get_last_bonds_reset.return_value ) - mocked_decode_block.assert_called_once_with(mocked_get_last_bonds_reset.return_value) assert result == mocked_decode_block.return_value @@ -1734,6 +1730,7 @@ def test_get_transfer_fee(subtensor, fake_wallet, mocker): fake_payment_info = {"partial_fee": int(2e10)} subtensor.substrate.get_payment_info.return_value = fake_payment_info + mocker_compose_call = mocker.patch.object(subtensor, "compose_call") # Call result = subtensor.get_transfer_fee( @@ -1741,14 +1738,14 @@ def test_get_transfer_fee(subtensor, fake_wallet, mocker): ) # Asserts - subtensor.substrate.compose_call.assert_called_once_with( + mocker_compose_call.assert_called_once_with( call_module="Balances", call_function="transfer_keep_alive", call_params={"dest": fake_dest, "value": value.rao}, ) subtensor.substrate.get_payment_info.assert_called_once_with( - call=subtensor.substrate.compose_call.return_value, + call=mocker_compose_call.return_value, keypair=fake_wallet.coldkeypub, ) @@ -3545,6 +3542,7 @@ def test_get_parents_no_parents(subtensor, mocker): def test_set_children(subtensor, fake_wallet, mocker): """Tests set_children extrinsic calls properly.""" # Preps + fake_netuid = mocker.Mock() mocked_set_children_extrinsic = mocker.Mock() mocker.patch.object( subtensor_module, "set_children_extrinsic", mocked_set_children_extrinsic @@ -3558,9 +3556,9 @@ def test_set_children(subtensor, fake_wallet, mocker): # Call result = subtensor.set_children( - fake_wallet, - fake_wallet.hotkey.ss58_address, - netuid=1, + wallet=fake_wallet, + netuid=fake_netuid, + hotkey_ss58=fake_wallet.hotkey.ss58_address, children=fake_children, ) @@ -3569,7 +3567,7 @@ def test_set_children(subtensor, fake_wallet, mocker): subtensor=subtensor, wallet=fake_wallet, hotkey_ss58=fake_wallet.hotkey.ss58_address, - netuid=1, + netuid=fake_netuid, children=fake_children, wait_for_finalization=True, wait_for_inclusion=True, @@ -3974,38 +3972,12 @@ def test_subnet(subtensor, mocker): assert result == mocked_di_from_dict.return_value -def test_get_stake_operations_fee(subtensor, mocker): - """Verify that `get_stake_operations_fee` calls proper methods and returns the correct value.""" - # Preps - netuid = 1 - amount = Balance.from_rao(100_000_000_000) # 100 Tao - mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") - mocked_query_map = mocker.patch.object( - subtensor.substrate, "query", return_value=mocker.Mock(value=196) - ) - - # Call - result = subtensor.get_stake_operations_fee(amount=amount, netuid=netuid) - - # Assert - mocked_determine_block_hash.assert_called_once_with(block=None) - mocked_query_map.assert_called_once_with( - module="Swap", - storage_function="FeeRate", - params=[netuid], - block_hash=mocked_determine_block_hash.return_value, - ) - assert result == Balance.from_rao(299076829) - - def test_get_stake_add_fee(subtensor, mocker): """Verify that `get_stake_add_fee` calls proper methods and returns the correct value.""" # Preps netuid = mocker.Mock() amount = mocker.Mock(spec=Balance) - mocked_get_stake_operations_fee = mocker.patch.object( - subtensor, "get_stake_operations_fee" - ) + mocked_sim_swap = mocker.patch.object(subtensor, "sim_swap") # Call result = subtensor.get_stake_add_fee( @@ -4014,10 +3986,13 @@ def test_get_stake_add_fee(subtensor, mocker): ) # Asserts - mocked_get_stake_operations_fee.assert_called_once_with( - amount=amount, netuid=netuid, block=None + mocked_sim_swap.assert_called_once_with( + origin_netuid=0, + destination_netuid=netuid, + amount=amount, + block=None, ) - assert result == mocked_get_stake_operations_fee.return_value + assert result == mocked_sim_swap.return_value.tao_fee def test_get_unstake_fee(subtensor, mocker): @@ -4025,8 +4000,11 @@ def test_get_unstake_fee(subtensor, mocker): # Preps netuid = mocker.Mock() amount = mocker.Mock(spec=Balance) - mocked_get_stake_operations_fee = mocker.patch.object( - subtensor, "get_stake_operations_fee" + mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + mocked_sim_swap = mocker.patch.object( + subtensor, + "sim_swap", + return_value=mocker.MagicMock(alpha_fee=mocker.MagicMock()), ) # Call @@ -4036,32 +4014,44 @@ def test_get_unstake_fee(subtensor, mocker): ) # Asserts - mocked_get_stake_operations_fee.assert_called_once_with( - amount=amount, netuid=netuid, block=None + mocked_sim_swap.assert_called_once_with( + origin_netuid=netuid, + destination_netuid=0, + amount=amount, + block=None, ) - assert result == mocked_get_stake_operations_fee.return_value + assert result == mocked_sim_swap.return_value.alpha_fee.set_unit.return_value def test_get_stake_movement_fee(subtensor, mocker): """Verify that `get_stake_movement_fee` calls proper methods and returns the correct value.""" # Preps - netuid = mocker.Mock() + origin_netuid = mocker.Mock() + destination_netuid = mocker.Mock() amount = mocker.Mock(spec=Balance) - mocked_get_stake_operations_fee = mocker.patch.object( - subtensor, "get_stake_operations_fee" + + mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + mocked_sim_swap = mocker.patch.object( + subtensor, + "sim_swap", + return_value=mocker.MagicMock(alpha_fee=mocker.MagicMock()), ) # Call result = subtensor.get_stake_movement_fee( + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, amount=amount, - origin_netuid=netuid, ) # Asserts - mocked_get_stake_operations_fee.assert_called_once_with( - amount=amount, netuid=netuid, block=None + mocked_sim_swap.assert_called_once_with( + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + amount=amount, + block=None, ) - assert result == mocked_get_stake_operations_fee.return_value + assert result == mocked_sim_swap.return_value.tao_fee def test_get_stake_weight(subtensor, mocker): @@ -4333,10 +4323,11 @@ def test_get_block_info(subtensor, mocker): }, "extrinsics": [ fake_decoded, - ] - + ], } - mocked_get_block = mocker.patch.object(subtensor.substrate, "get_block", return_value=fake_substrate_block) + mocked_get_block = mocker.patch.object( + subtensor.substrate, "get_block", return_value=fake_substrate_block + ) mocked_BlockInfo = mocker.patch.object(subtensor_module, "BlockInfo") # Call @@ -4354,6 +4345,6 @@ def test_get_block_info(subtensor, mocker): timestamp=fake_timestamp, header=fake_substrate_block.get("header"), extrinsics=fake_substrate_block.get("extrinsics"), - explorer=f"{settings.TAO_APP_BLOCK_EXPLORER}{fake_block}" + explorer=f"{settings.TAO_APP_BLOCK_EXPLORER}{fake_block}", ) assert result == mocked_BlockInfo.return_value