From f499c995d91552d073352421b5462d8157908724 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Fri, 7 Nov 2025 10:32:11 -0800 Subject: [PATCH 01/62] add chain data proxy --- bittensor/core/chain_data/__init__.py | 1 + bittensor/core/chain_data/proxy.py | 88 +++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 bittensor/core/chain_data/proxy.py diff --git a/bittensor/core/chain_data/__init__.py b/bittensor/core/chain_data/__init__.py index a232d8b651..6b0d614d4a 100644 --- a/bittensor/core/chain_data/__init__.py +++ b/bittensor/core/chain_data/__init__.py @@ -23,6 +23,7 @@ from .neuron_info_lite import NeuronInfoLite from .prometheus_info import PrometheusInfo from .proposal_vote_data import ProposalVoteData +from .proxy import ProxyConstants, ProxyInfo from .scheduled_coldkey_swap_info import ScheduledColdkeySwapInfo from .stake_info import StakeInfo from .sim_swap import SimSwapResult diff --git a/bittensor/core/chain_data/proxy.py b/bittensor/core/chain_data/proxy.py new file mode 100644 index 0000000000..95ec744c3a --- /dev/null +++ b/bittensor/core/chain_data/proxy.py @@ -0,0 +1,88 @@ +from bittensor.core.chain_data.utils import decode_account_id +from dataclasses import dataclass +from typing import Any, Optional +from bittensor.utils.balance import Balance + + +@dataclass +class ProxyInfo: + delegate: str + proxy_type: str + delay: int + + @classmethod + def from_dict(cls, data: dict): + """Returns a ProxyInfo object from proxy data.""" + return cls( + delegate=decode_account_id(data["delegate"]), + proxy_type=data["proxy_type"], + delay=data["delay"], + ) + + @classmethod + def from_tuple(cls, data: tuple): + """Returns a list of ProxyInfo objects from a tuple of proxy data.""" + return [ + cls( + delegate=decode_account_id(proxy["delegate"]), + proxy_type=proxy["proxy_type"], + delay=proxy["delay"], + ) + for proxy in data + ] + + @classmethod + def from_query(cls, query: Any): + """Returns a ProxyInfo object from a Substrate query.""" + try: + proxies = query.value[0][0] + balance = query.value[1] + return cls.from_tuple(proxies), Balance.from_rao(balance) + except IndexError: + return [], Balance.from_rao(0) + + +@dataclass +class ProxyConstants: + """ + Represents all runtime constants defined in the `Proxy` pallet. + + + Attributes: + + + Note: + All Balance amounts are in RAO. + """ + + AnnouncementDepositBase: Optional[Balance] + AnnouncementDepositFactor: Optional[Balance] + MaxProxies: Optional[int] + MaxPending: Optional[int] + ProxyDepositBase: Optional[Balance] + ProxyDepositFactor: Optional[Balance] + + @classmethod + def constants_names(cls) -> list[str]: + """Returns the list of all constant field names defined in this dataclass.""" + from dataclasses import fields + + return [f.name for f in fields(cls)] + + @classmethod + def from_dict(cls, data: dict) -> "ProxyConstants": + """ + Creates a `ProxyConstants` instance from a dictionary of decoded chain constants. + + Parameters: + data: Dictionary mapping constant names to their decoded values (returned by `Subtensor.query_constant()`). + + Returns: + ProxyConstants: The structured dataclass with constants filled in. + """ + return cls(**{name: data.get(name) for name in cls.constants_names()}) + + def to_dict(self) -> dict: + from dataclasses import asdict + + return asdict(self) From fb35271a3cdf88b07c23a65df7e877bdd8b38ff5 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Fri, 7 Nov 2025 10:39:18 -0800 Subject: [PATCH 02/62] add query methods --- bittensor/core/subtensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index d658675ed3..2a3d349513 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -23,6 +23,8 @@ MetagraphInfo, NeuronInfo, NeuronInfoLite, + ProxyInfo, + ProxyConstants, SelectiveMetagraphIndex, SimSwapResult, StakeInfo, From 4bcc6e8b629a9bd8e1f7faf411040e5c7c98113c Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Fri, 7 Nov 2025 10:45:53 -0800 Subject: [PATCH 03/62] update __all__ --- bittensor/core/chain_data/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bittensor/core/chain_data/__init__.py b/bittensor/core/chain_data/__init__.py index 6b0d614d4a..196e6e61c7 100644 --- a/bittensor/core/chain_data/__init__.py +++ b/bittensor/core/chain_data/__init__.py @@ -55,6 +55,8 @@ "PrometheusInfo", "ProposalCallData", "ProposalVoteData", + "ProxyConstants", + "ProxyInfo", "ScheduledColdkeySwapInfo", "SelectiveMetagraphIndex", "SimSwapResult", From b128c20b4bf2fe4a7e75641ce558a95d0965650c Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Fri, 7 Nov 2025 10:50:46 -0800 Subject: [PATCH 04/62] add bittensor/core/extrinsics/pallets/proxy.py pallet --- bittensor/core/extrinsics/pallets/proxy.py | 162 +++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 bittensor/core/extrinsics/pallets/proxy.py diff --git a/bittensor/core/extrinsics/pallets/proxy.py b/bittensor/core/extrinsics/pallets/proxy.py new file mode 100644 index 0000000000..9a6bcfb2a3 --- /dev/null +++ b/bittensor/core/extrinsics/pallets/proxy.py @@ -0,0 +1,162 @@ +from dataclasses import dataclass +from typing import TYPE_CHECKING, Optional + +from .base import CallBuilder as _BasePallet, Call + +if TYPE_CHECKING: + from scalecodec import GenericCall + + +@dataclass +class Proxy(_BasePallet): + """Factory class for creating GenericCall objects for Proxy pallet functions. + + This class provides methods to create GenericCall instances for all Proxy pallet extrinsics. + + Works with both sync (Subtensor) and async (AsyncSubtensor) instances. For async operations, pass an AsyncSubtensor + instance and await the result. + + Example: + # Sync usage + call = Proxy(subtensor).add_proxy(delegate="5DE..", proxy_type="Any", delay=0) + response = subtensor.sign_and_send_extrinsic(call=call, ...) + + # Async usage + call = await Proxy(async_subtensor).add_proxy(delegate="5DE..", proxy_type="Any", delay=0) + response = await async_subtensor.sign_and_send_extrinsic(call=call, ...) + """ + + def add_proxy( + self, + delegate: str, + proxy_type: str, + delay: int, + ) -> Call: + """Returns GenericCall instance for Subtensor function Proxy.add_proxy. + + Parameters: + delegate: The SS58 address of the delegate proxy account. + proxy_type: The type of proxy permissions (e.g., "Any", "NonTransfer", "Governance", "Staking", "IdentityJudgement", "CancelProxy", "Auction"). + delay: The number of blocks before the proxy can be used. + + Returns: + GenericCall instance. + """ + return self.create_composed_call( + delegate=delegate, + proxy_type=proxy_type, + delay=delay, + ) + + def remove_proxy( + self, + delegate: str, + proxy_type: str, + delay: int, + ) -> Call: + """Returns GenericCall instance for Subtensor function Proxy.remove_proxy. + + Parameters: + delegate: The SS58 address of the delegate proxy account to remove. + proxy_type: The type of proxy permissions to remove (e.g., "Any", "NonTransfer", "Governance", "Staking", "IdentityJudgement", "CancelProxy", "Auction"). + delay: The number of blocks before the proxy removal takes effect. + + Returns: + GenericCall instance. + """ + return self.create_composed_call( + delegate=delegate, + proxy_type=proxy_type, + delay=delay, + ) + + def create_pure( + self, + proxy_type: str, + delay: int, + index: int, + ) -> Call: + """Returns GenericCall instance for Subtensor function Proxy.create_pure. + + Parameters: + proxy_type: The type of proxy permissions for the pure proxy (e.g., "Any", "NonTransfer", "Governance", "Staking", "IdentityJudgement", "CancelProxy", "Auction"). + delay: The number of blocks before the pure proxy can be used. + index: The index to use for generating the pure proxy account address. + + Returns: + GenericCall instance. + """ + return self.create_composed_call( + proxy_type=proxy_type, + delay=delay, + index=index, + ) + + def kill_pure( + self, + spawner: str, + proxy: str, + proxy_type: str, + height: int, + ext_index: int, + ) -> Call: + """Returns GenericCall instance for Subtensor function Proxy.kill_pure. + + Parameters: + spawner: The SS58 address of the account that spawned the pure proxy. + proxy: The SS58 address of the pure proxy account to kill. + proxy_type: The type of proxy permissions (e.g., "Any", "NonTransfer", "Governance", "Staking", "IdentityJudgement", "CancelProxy", "Auction"). + height: The block height at which the pure proxy was created. + ext_index: The extrinsic index at which the pure proxy was created. + + Returns: + GenericCall instance. + """ + return self.create_composed_call( + spawner=spawner, + proxy=proxy, + proxy_type=proxy_type, + height=height, + ext_index=ext_index, + ) + + def proxy( + self, + real: str, + force_proxy_type: Optional[str], + call: "GenericCall", + ) -> Call: + """Returns GenericCall instance for Subtensor function Proxy.proxy. + + Parameters: + real: The SS58 address of the real account on whose behalf the call is being made. + force_proxy_type: The type of proxy to use for the call. If None, any proxy type can be used. Otherwise, must match one of the allowed proxy types (e.g., "Any", "NonTransfer", "Governance", "Staking", "IdentityJudgement", "CancelProxy", "Auction"). + call: The inner call to be executed on behalf of the real account. + + Returns: + GenericCall instance. + """ + return self.create_composed_call( + real=real, + force_proxy_type=force_proxy_type, + call=call, + ) + + def announce( + self, + real: str, + call_hash: str, + ) -> Call: + """Returns GenericCall instance for Subtensor function Proxy.announce. + + Parameters: + real: The SS58 address of the real account on whose behalf the call will be made. + call_hash: The hash of the call that will be executed in the future. + + Returns: + GenericCall instance. + """ + return self.create_composed_call( + real=real, + call_hash=call_hash, + ) From 50be8e058dc828e7fcfc41b8b647dcff6bd790d8 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Fri, 7 Nov 2025 10:59:47 -0800 Subject: [PATCH 05/62] ruff --- bittensor/core/extrinsics/pallets/__init__.py | 2 ++ bittensor/core/extrinsics/pallets/proxy.py | 16 +++++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/bittensor/core/extrinsics/pallets/__init__.py b/bittensor/core/extrinsics/pallets/__init__.py index af2a987554..facf04b2b8 100644 --- a/bittensor/core/extrinsics/pallets/__init__.py +++ b/bittensor/core/extrinsics/pallets/__init__.py @@ -2,6 +2,7 @@ from .balances import Balances from .commitments import Commitments from .crowdloan import Crowdloan +from .proxy import Proxy from .subtensor_module import SubtensorModule from .sudo import Sudo from .swap import Swap @@ -12,6 +13,7 @@ "Balances", "Commitments", "Crowdloan", + "Proxy", "SubtensorModule", "Sudo", "Swap", diff --git a/bittensor/core/extrinsics/pallets/proxy.py b/bittensor/core/extrinsics/pallets/proxy.py index 9a6bcfb2a3..19c401f983 100644 --- a/bittensor/core/extrinsics/pallets/proxy.py +++ b/bittensor/core/extrinsics/pallets/proxy.py @@ -36,7 +36,8 @@ def add_proxy( Parameters: delegate: The SS58 address of the delegate proxy account. - proxy_type: The type of proxy permissions (e.g., "Any", "NonTransfer", "Governance", "Staking", "IdentityJudgement", "CancelProxy", "Auction"). + proxy_type: The type of proxy permissions (e.g., "Any", "NonTransfer", "Governance", "Staking", + "IdentityJudgement", "CancelProxy", "Auction"). delay: The number of blocks before the proxy can be used. Returns: @@ -58,7 +59,8 @@ def remove_proxy( Parameters: delegate: The SS58 address of the delegate proxy account to remove. - proxy_type: The type of proxy permissions to remove (e.g., "Any", "NonTransfer", "Governance", "Staking", "IdentityJudgement", "CancelProxy", "Auction"). + proxy_type: The type of proxy permissions to remove (e.g., "Any", "NonTransfer", "Governance", "Staking", + "IdentityJudgement", "CancelProxy", "Auction"). delay: The number of blocks before the proxy removal takes effect. Returns: @@ -79,7 +81,8 @@ def create_pure( """Returns GenericCall instance for Subtensor function Proxy.create_pure. Parameters: - proxy_type: The type of proxy permissions for the pure proxy (e.g., "Any", "NonTransfer", "Governance", "Staking", "IdentityJudgement", "CancelProxy", "Auction"). + proxy_type: The type of proxy permissions for the pure proxy (e.g., "Any", "NonTransfer", "Governance", + "Staking", "IdentityJudgement", "CancelProxy", "Auction"). delay: The number of blocks before the pure proxy can be used. index: The index to use for generating the pure proxy account address. @@ -105,7 +108,8 @@ def kill_pure( Parameters: spawner: The SS58 address of the account that spawned the pure proxy. proxy: The SS58 address of the pure proxy account to kill. - proxy_type: The type of proxy permissions (e.g., "Any", "NonTransfer", "Governance", "Staking", "IdentityJudgement", "CancelProxy", "Auction"). + proxy_type: The type of proxy permissions (e.g., "Any", "NonTransfer", "Governance", "Staking", + "IdentityJudgement", "CancelProxy", "Auction"). height: The block height at which the pure proxy was created. ext_index: The extrinsic index at which the pure proxy was created. @@ -130,7 +134,9 @@ def proxy( Parameters: real: The SS58 address of the real account on whose behalf the call is being made. - force_proxy_type: The type of proxy to use for the call. If None, any proxy type can be used. Otherwise, must match one of the allowed proxy types (e.g., "Any", "NonTransfer", "Governance", "Staking", "IdentityJudgement", "CancelProxy", "Auction"). + force_proxy_type: The type of proxy to use for the call. If None, any proxy type can be used. Otherwise, + must match one of the allowed proxy types (e.g., "Any", "NonTransfer", "Governance", "Staking", + "IdentityJudgement", "CancelProxy", "Auction"). call: The inner call to be executed on behalf of the real account. Returns: From 2c0cd977a6bd9dc8b909d59225bfb7012dcddd8d Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Fri, 7 Nov 2025 11:16:46 -0800 Subject: [PATCH 06/62] add ProxyType --- bittensor/core/chain_data/__init__.py | 3 +- bittensor/core/chain_data/proxy.py | 80 ++++++++++++++++++++++++++- 2 files changed, 79 insertions(+), 4 deletions(-) diff --git a/bittensor/core/chain_data/__init__.py b/bittensor/core/chain_data/__init__.py index 196e6e61c7..eb16ed305c 100644 --- a/bittensor/core/chain_data/__init__.py +++ b/bittensor/core/chain_data/__init__.py @@ -23,7 +23,7 @@ from .neuron_info_lite import NeuronInfoLite from .prometheus_info import PrometheusInfo from .proposal_vote_data import ProposalVoteData -from .proxy import ProxyConstants, ProxyInfo +from .proxy import ProxyConstants, ProxyInfo, ProxyType from .scheduled_coldkey_swap_info import ScheduledColdkeySwapInfo from .stake_info import StakeInfo from .sim_swap import SimSwapResult @@ -57,6 +57,7 @@ "ProposalVoteData", "ProxyConstants", "ProxyInfo", + "ProxyType", "ScheduledColdkeySwapInfo", "SelectiveMetagraphIndex", "SimSwapResult", diff --git a/bittensor/core/chain_data/proxy.py b/bittensor/core/chain_data/proxy.py index 95ec744c3a..4a628703f6 100644 --- a/bittensor/core/chain_data/proxy.py +++ b/bittensor/core/chain_data/proxy.py @@ -1,9 +1,84 @@ -from bittensor.core.chain_data.utils import decode_account_id from dataclasses import dataclass -from typing import Any, Optional +from enum import Enum +from typing import Any, Optional, Union + +from bittensor.core.chain_data.utils import decode_account_id from bittensor.utils.balance import Balance +class ProxyType(str, Enum): + """ + Enumeration of all supported proxy types in the Bittensor network. + + These types define the permissions that a proxy account has when acting on behalf of the real account. Each type + restricts what operations the proxy can perform. + + Note: + The values match exactly with the ProxyType enum defined in the Subtensor runtime. Any changes to the runtime + enum must be reflected here. + """ + + any = "Any" + Owner = "Owner" + NonCritical = "NonCritical" + NonTransfer = "NonTransfer" + Senate = "Senate" + NonFungible = "NonFungible" + Triumvirate = "Triumvirate" + Governance = "Governance" + Staking = "Staking" + Registration = "Registration" + Transfer = "Transfer" + SmallTransfer = "SmallTransfer" + RootWeights = "RootWeights" + ChildKeys = "ChildKeys" + SudoUncheckedSetCode = "SudoUncheckedSetCode" + SwapHotkey = "SwapHotkey" + SubnetLeaseBeneficiary = "SubnetLeaseBeneficiary" + RootClaim = "RootClaim" + + @classmethod + def all_types(cls) -> list[str]: + """Returns a list of all proxy type values.""" + return [member.value for member in cls] + + @classmethod + def is_valid(cls, value: str) -> bool: + """Checks if a string value is a valid proxy type.""" + return value in cls.all_types() + + @classmethod + def normalize(cls, proxy_type: Union[str, "ProxyType"]) -> str: + """ + Normalizes a proxy type to a string value. + + This method handles both string and ProxyType enum values, validates the input, and returns the string + representation suitable for Substrate calls. + + Parameters: + proxy_type: Either a string or ProxyType enum value. + + Returns: + str: The normalized string value of the proxy type. + + Raises: + ValueError: If the proxy_type is not a valid proxy type. + """ + if isinstance(proxy_type, ProxyType): + return proxy_type.value + elif isinstance(proxy_type, str): + if not cls.is_valid(proxy_type): + raise ValueError( + f"Invalid proxy type: {proxy_type}. " + f"Valid types are: {', '.join(cls.all_types())}" + ) + return proxy_type + else: + raise TypeError( + f"proxy_type must be str or ProxyType, got {type(proxy_type).__name__}" + ) + + @dataclass class ProxyInfo: delegate: str @@ -47,7 +122,6 @@ class ProxyConstants: """ Represents all runtime constants defined in the `Proxy` pallet. - Attributes: From 53761b5a6a4a89a941fd2a2480fc76e33d7adb27 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Fri, 7 Nov 2025 11:17:54 -0800 Subject: [PATCH 07/62] refactor pallet --- bittensor/core/extrinsics/pallets/proxy.py | 44 +++++++++++----------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/bittensor/core/extrinsics/pallets/proxy.py b/bittensor/core/extrinsics/pallets/proxy.py index 19c401f983..dfae7b5ed7 100644 --- a/bittensor/core/extrinsics/pallets/proxy.py +++ b/bittensor/core/extrinsics/pallets/proxy.py @@ -1,7 +1,8 @@ from dataclasses import dataclass -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, Union from .base import CallBuilder as _BasePallet, Call +from bittensor.core.chain_data.proxy import ProxyType if TYPE_CHECKING: from scalecodec import GenericCall @@ -29,68 +30,69 @@ class Proxy(_BasePallet): def add_proxy( self, delegate: str, - proxy_type: str, + proxy_type: Union[str, ProxyType], delay: int, ) -> Call: """Returns GenericCall instance for Subtensor function Proxy.add_proxy. Parameters: delegate: The SS58 address of the delegate proxy account. - proxy_type: The type of proxy permissions (e.g., "Any", "NonTransfer", "Governance", "Staking", - "IdentityJudgement", "CancelProxy", "Auction"). + proxy_type: The type of proxy permissions (e.g., "Any", "NonTransfer", "Governance", "Staking"). Can be a + string or ProxyType enum value. delay: The number of blocks before the proxy can be used. Returns: GenericCall instance. """ + proxy_type_str = ProxyType.normalize(proxy_type) return self.create_composed_call( delegate=delegate, - proxy_type=proxy_type, + proxy_type=proxy_type_str, delay=delay, ) def remove_proxy( self, delegate: str, - proxy_type: str, + proxy_type: Union[str, ProxyType], delay: int, ) -> Call: """Returns GenericCall instance for Subtensor function Proxy.remove_proxy. Parameters: delegate: The SS58 address of the delegate proxy account to remove. - proxy_type: The type of proxy permissions to remove (e.g., "Any", "NonTransfer", "Governance", "Staking", - "IdentityJudgement", "CancelProxy", "Auction"). + proxy_type: The type of proxy permissions to remove. Can be a string or ProxyType enum value. delay: The number of blocks before the proxy removal takes effect. Returns: GenericCall instance. """ + proxy_type_str = ProxyType.normalize(proxy_type) return self.create_composed_call( delegate=delegate, - proxy_type=proxy_type, + proxy_type=proxy_type_str, delay=delay, ) def create_pure( self, - proxy_type: str, + proxy_type: Union[str, ProxyType], delay: int, index: int, ) -> Call: """Returns GenericCall instance for Subtensor function Proxy.create_pure. Parameters: - proxy_type: The type of proxy permissions for the pure proxy (e.g., "Any", "NonTransfer", "Governance", - "Staking", "IdentityJudgement", "CancelProxy", "Auction"). + proxy_type: The type of proxy permissions for the pure proxy. Can be a string or ProxyType enum value. delay: The number of blocks before the pure proxy can be used. index: The index to use for generating the pure proxy account address. Returns: GenericCall instance. """ + proxy_type_str = ProxyType.normalize(proxy_type) return self.create_composed_call( - proxy_type=proxy_type, + proxy_type=proxy_type_str, delay=delay, index=index, ) @@ -99,7 +101,7 @@ def kill_pure( self, spawner: str, proxy: str, - proxy_type: str, + proxy_type: Union[str, ProxyType], height: int, ext_index: int, ) -> Call: @@ -108,18 +110,18 @@ def kill_pure( Parameters: spawner: The SS58 address of the account that spawned the pure proxy. proxy: The SS58 address of the pure proxy account to kill. - proxy_type: The type of proxy permissions (e.g., "Any", "NonTransfer", "Governance", "Staking", - "IdentityJudgement", "CancelProxy", "Auction"). + proxy_type: The type of proxy permissions. Can be a string or ProxyType enum value. height: The block height at which the pure proxy was created. ext_index: The extrinsic index at which the pure proxy was created. Returns: GenericCall instance. """ + proxy_type_str = ProxyType.normalize(proxy_type) return self.create_composed_call( spawner=spawner, proxy=proxy, - proxy_type=proxy_type, + proxy_type=proxy_type_str, height=height, ext_index=ext_index, ) @@ -127,7 +129,7 @@ def kill_pure( def proxy( self, real: str, - force_proxy_type: Optional[str], + force_proxy_type: Optional[Union[str, ProxyType]], call: "GenericCall", ) -> Call: """Returns GenericCall instance for Subtensor function Proxy.proxy. @@ -135,16 +137,16 @@ def proxy( Parameters: real: The SS58 address of the real account on whose behalf the call is being made. force_proxy_type: The type of proxy to use for the call. If None, any proxy type can be used. Otherwise, - must match one of the allowed proxy types (e.g., "Any", "NonTransfer", "Governance", "Staking", - "IdentityJudgement", "CancelProxy", "Auction"). + must match one of the allowed proxy types. Can be a string or ProxyType enum value. call: The inner call to be executed on behalf of the real account. Returns: GenericCall instance. """ + force_proxy_type_str = ProxyType.normalize(force_proxy_type) if force_proxy_type is not None else None return self.create_composed_call( real=real, - force_proxy_type=force_proxy_type, + force_proxy_type=force_proxy_type_str, call=call, ) From 8161fdf6837591e142248516cbc0601a38ba4200 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Fri, 7 Nov 2025 11:25:19 -0800 Subject: [PATCH 08/62] improve bittensor/core/chain_data/proxy.py --- bittensor/core/chain_data/proxy.py | 35 +++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/bittensor/core/chain_data/proxy.py b/bittensor/core/chain_data/proxy.py index 4a628703f6..8177e942a9 100644 --- a/bittensor/core/chain_data/proxy.py +++ b/bittensor/core/chain_data/proxy.py @@ -81,6 +81,20 @@ def normalize(cls, proxy_type: Union[str, "ProxyType"]) -> str: @dataclass class ProxyInfo: + """ + Dataclass representing proxy relationship information. + + This class contains information about a proxy relationship between a real account and a delegate account. A proxy + relationship allows the delegate to perform certain operations on behalf of the real account, with restrictions + defined by the proxy type and a delay period. + + Attributes: + delegate: The SS58 address of the delegate proxy account that can act on behalf of the real account. + proxy_type: The type of proxy permissions granted to the delegate (e.g., "Any", "NonTransfer", "Governance", + "Staking"). This determines what operations the delegate can perform. + delay: The number of blocks that must pass before the proxy relationship becomes active. This delay provides a + security mechanism allowing the real account to cancel the proxy if needed. + """ delegate: str proxy_type: str delay: int @@ -122,8 +136,27 @@ class ProxyConstants: """ Represents all runtime constants defined in the `Proxy` pallet. - Attributes: + These attributes correspond directly to on-chain configuration constants exposed by the Proxy pallet. They define + deposit requirements, proxy limits, and announcement constraints that govern how proxy accounts operate within the + Subtensor network. + Each attribute is fetched directly from the runtime via `Subtensor.query_constant("Proxy", )` and reflects the + current chain configuration at the time of retrieval. + + Attributes: + AnnouncementDepositBase: Base deposit amount (in RAO) required to announce a future proxy call. This deposit is + held until the announced call is executed or cancelled. + AnnouncementDepositFactor: Additional deposit factor (in RAO) per byte of the call hash being announced. The + total announcement deposit is calculated as: AnnouncementDepositBase + (call_hash_size * + AnnouncementDepositFactor). + MaxProxies: Maximum number of proxy relationships that a single account can have. This limits the total number + of delegates that can act on behalf of an account. + MaxPending: Maximum number of pending proxy announcements that can exist for a single account at any given time. + This prevents spam and limits the storage requirements for pending announcements. + ProxyDepositBase: Base deposit amount (in RAO) required when adding a proxy relationship. This deposit is held as + long as the proxy relationship exists and is returned when the proxy is removed. + ProxyDepositFactor: Additional deposit factor (in RAO) per proxy type added. The total proxy deposit is + calculated as: ProxyDepositBase + (number_of_proxy_types * ProxyDepositFactor). Note: All Balance amounts are in RAO. From 1af667c451efc20e55e1d19eef99d2986442b84f Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Fri, 7 Nov 2025 11:27:46 -0800 Subject: [PATCH 09/62] add sync `proxy` extrinsics --- bittensor/core/extrinsics/proxy.py | 436 +++++++++++++++++++++++++++++ 1 file changed, 436 insertions(+) create mode 100644 bittensor/core/extrinsics/proxy.py diff --git a/bittensor/core/extrinsics/proxy.py b/bittensor/core/extrinsics/proxy.py new file mode 100644 index 0000000000..b038bea830 --- /dev/null +++ b/bittensor/core/extrinsics/proxy.py @@ -0,0 +1,436 @@ +from typing import TYPE_CHECKING, Optional, Union + +from bittensor.core.extrinsics.pallets import Proxy +from bittensor.core.chain_data.proxy import ProxyType +from bittensor.core.types import ExtrinsicResponse +from bittensor.utils.btlogging import logging + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.subtensor import Subtensor + from scalecodec.types import GenericCall + + +def add_proxy_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + delegate_ss58: str, + proxy_type: Union[str, ProxyType], + delay: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> ExtrinsicResponse: + """ + Adds a proxy relationship. + + Parameters: + subtensor: Subtensor instance with the connection to the chain. + wallet: Bittensor wallet object. + delegate_ss58: The SS58 address of the delegate proxy account. + proxy_type: The type of proxy permissions (e.g., "Any", "NonTransfer", "Governance", "Staking"). Can be a + string or ProxyType enum value. + delay: The number of blocks before the proxy can be used. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + proxy_type_str = ProxyType.normalize(proxy_type) + + logging.debug( + f"Adding proxy: delegate=[blue]{delegate_ss58}[/blue], " + f"type=[blue]{proxy_type_str}[/blue], delay=[blue]{delay}[/blue] " + f"on [blue]{subtensor.network}[/blue]." + ) + + call = Proxy(subtensor).add_proxy( + delegate=delegate_ss58, + proxy_type=proxy_type_str, + delay=delay, + ) + + response = 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, + ) + + if response.success: + logging.debug("[green]Proxy added successfully.[/green]") + else: + logging.error(f"[red]{response.message}[/red]") + + return response + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + +def announce_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + real_ss58: str, + call_hash: str, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> ExtrinsicResponse: + """ + Announces a future call that will be executed through a proxy. + + Parameters: + subtensor: Subtensor instance with the connection to the chain. + wallet: Bittensor wallet object (should be the proxy account wallet). + real_ss58: The SS58 address of the real account on whose behalf the call will be made. + call_hash: The hash of the call that will be executed in the future. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + logging.debug( + f"Announcing proxy call: real=[blue]{real_ss58}[/blue], " + f"call_hash=[blue]{call_hash}[/blue] " + f"on [blue]{subtensor.network}[/blue]." + ) + + call = Proxy(subtensor).announce( + real=real_ss58, + call_hash=call_hash, + ) + + response = 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, + ) + + if response.success: + logging.debug("[green]Proxy call announced successfully.[/green]") + else: + logging.error(f"[red]{response.message}[/red]") + + return response + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + +def create_pure_proxy_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + proxy_type: Union[str, ProxyType], + delay: int, + index: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> ExtrinsicResponse: + """ + Creates a pure proxy account. + + Parameters: + subtensor: Subtensor instance with the connection to the chain. + wallet: Bittensor wallet object. + proxy_type: The type of proxy permissions for the pure proxy. Can be a string or ProxyType enum value. + delay: The number of blocks before the pure proxy can be used. + index: The index to use for generating the pure proxy account address. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + proxy_type_str = ProxyType.normalize(proxy_type) + + logging.debug( + f"Creating pure proxy: type=[blue]{proxy_type_str}[/blue], " + f"delay=[blue]{delay}[/blue], index=[blue]{index}[/blue] " + f"on [blue]{subtensor.network}[/blue]." + ) + + call = Proxy(subtensor).create_pure( + proxy_type=proxy_type_str, + delay=delay, + index=index, + ) + + response = 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, + ) + + if response.success: + logging.debug("[green]Pure proxy created successfully.[/green]") + else: + logging.error(f"[red]{response.message}[/red]") + + return response + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + +def kill_pure_proxy_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + spawner_ss58: str, + proxy_ss58: str, + proxy_type: Union[str, ProxyType], + height: int, + ext_index: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> ExtrinsicResponse: + """ + Kills (removes) a pure proxy account. + + Parameters: + subtensor: Subtensor instance with the connection to the chain. + wallet: Bittensor wallet object. + spawner_ss58: The SS58 address of the account that spawned the pure proxy. + proxy_ss58: The SS58 address of the pure proxy account to kill. + proxy_type: The type of proxy permissions. Can be a string or ProxyType enum value. + height: The block height at which the pure proxy was created. + ext_index: The extrinsic index at which the pure proxy was created. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + proxy_type_str = ProxyType.normalize(proxy_type) + + logging.debug( + f"Killing pure proxy: spawner=[blue]{spawner_ss58}[/blue], " + f"proxy=[blue]{proxy_ss58}[/blue], type=[blue]{proxy_type_str}[/blue] " + f"on [blue]{subtensor.network}[/blue]." + ) + + call = Proxy(subtensor).kill_pure( + spawner=spawner_ss58, + proxy=proxy_ss58, + proxy_type=proxy_type_str, + height=height, + ext_index=ext_index, + ) + + response = 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, + ) + + if response.success: + logging.debug("[green]Pure proxy killed successfully.[/green]") + else: + logging.error(f"[red]{response.message}[/red]") + + return response + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + +def proxy_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + real_ss58: str, + force_proxy_type: Optional[Union[str, ProxyType]], + call: "GenericCall", + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> ExtrinsicResponse: + """ + Executes a call on behalf of the real account through a proxy. + + Parameters: + subtensor: Subtensor instance with the connection to the chain. + wallet: Bittensor wallet object (should be the proxy account wallet). + real_ss58: The SS58 address of the real account on whose behalf the call is being made. + force_proxy_type: The type of proxy to use for the call. If None, any proxy type can be used. Otherwise, must + match one of the allowed proxy types. Can be a string or ProxyType enum value. + call: The inner call to be executed on behalf of the real account. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + force_proxy_type_str = ( + ProxyType.normalize(force_proxy_type) if force_proxy_type is not None else None + ) + + logging.debug( + f"Executing proxy call: real=[blue]{real_ss58}[/blue], " + f"force_type=[blue]{force_proxy_type_str}[/blue] " + f"on [blue]{subtensor.network}[/blue]." + ) + + proxy_call = Proxy(subtensor).proxy( + real=real_ss58, + force_proxy_type=force_proxy_type_str, + call=call, + ) + + response = subtensor.sign_and_send_extrinsic( + call=proxy_call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + raise_error=raise_error, + ) + + if response.success: + logging.debug("[green]Proxy call executed successfully.[/green]") + else: + logging.error(f"[red]{response.message}[/red]") + + return response + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + +def remove_proxy_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + delegate_ss58: str, + proxy_type: Union[str, ProxyType], + delay: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> ExtrinsicResponse: + """ + Removes a proxy relationship. + + Parameters: + subtensor: Subtensor instance with the connection to the chain. + wallet: Bittensor wallet object. + delegate_ss58: The SS58 address of the delegate proxy account to remove. + proxy_type: The type of proxy permissions to remove. Can be a string or ProxyType enum value. + delay: The number of blocks before the proxy removal takes effect. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + proxy_type_str = ProxyType.normalize(proxy_type) + + logging.debug( + f"Removing proxy: delegate=[blue]{delegate_ss58}[/blue], " + f"type=[blue]{proxy_type_str}[/blue], delay=[blue]{delay}[/blue] " + f"on [blue]{subtensor.network}[/blue]." + ) + + call = Proxy(subtensor).remove_proxy( + delegate=delegate_ss58, + proxy_type=proxy_type_str, + delay=delay, + ) + + response = 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, + ) + + if response.success: + logging.debug("[green]Proxy removed successfully.[/green]") + else: + logging.error(f"[red]{response.message}[/red]") + + return response + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) From bd4f165de7f3d8f900eddced5a7600d3f03a2e32 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Fri, 7 Nov 2025 11:38:33 -0800 Subject: [PATCH 10/62] clean signatures for pallet methods --- bittensor/core/extrinsics/pallets/proxy.py | 103 ++++++++++++++++----- 1 file changed, 80 insertions(+), 23 deletions(-) diff --git a/bittensor/core/extrinsics/pallets/proxy.py b/bittensor/core/extrinsics/pallets/proxy.py index dfae7b5ed7..8a18f3cf75 100644 --- a/bittensor/core/extrinsics/pallets/proxy.py +++ b/bittensor/core/extrinsics/pallets/proxy.py @@ -1,8 +1,7 @@ from dataclasses import dataclass -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING, Optional from .base import CallBuilder as _BasePallet, Call -from bittensor.core.chain_data.proxy import ProxyType if TYPE_CHECKING: from scalecodec import GenericCall @@ -30,69 +29,65 @@ class Proxy(_BasePallet): def add_proxy( self, delegate: str, - proxy_type: Union[str, ProxyType], + proxy_type: str, delay: int, ) -> Call: """Returns GenericCall instance for Subtensor function Proxy.add_proxy. Parameters: delegate: The SS58 address of the delegate proxy account. - proxy_type: The type of proxy permissions (e.g., "Any", "NonTransfer", "Governance", "Staking"). Can be a - string or ProxyType enum value. + proxy_type: The type of proxy permissions (e.g., "Any", "NonTransfer", "Governance", "Staking"). delay: The number of blocks before the proxy can be used. Returns: GenericCall instance. """ - proxy_type_str = ProxyType.normalize(proxy_type) return self.create_composed_call( delegate=delegate, - proxy_type=proxy_type_str, + proxy_type=proxy_type, delay=delay, ) def remove_proxy( self, delegate: str, - proxy_type: Union[str, ProxyType], + proxy_type: str, delay: int, ) -> Call: """Returns GenericCall instance for Subtensor function Proxy.remove_proxy. Parameters: delegate: The SS58 address of the delegate proxy account to remove. - proxy_type: The type of proxy permissions to remove. Can be a string or ProxyType enum value. + proxy_type: The type of proxy permissions to remove. delay: The number of blocks before the proxy removal takes effect. Returns: GenericCall instance. """ - proxy_type_str = ProxyType.normalize(proxy_type) return self.create_composed_call( delegate=delegate, - proxy_type=proxy_type_str, + proxy_type=proxy_type, delay=delay, ) def create_pure( self, - proxy_type: Union[str, ProxyType], + proxy_type: str, delay: int, index: int, ) -> Call: """Returns GenericCall instance for Subtensor function Proxy.create_pure. Parameters: - proxy_type: The type of proxy permissions for the pure proxy. Can be a string or ProxyType enum value. + proxy_type: The type of proxy permissions for the pure proxy. delay: The number of blocks before the pure proxy can be used. index: The index to use for generating the pure proxy account address. Returns: GenericCall instance. """ - proxy_type_str = ProxyType.normalize(proxy_type) return self.create_composed_call( - proxy_type=proxy_type_str, + proxy_type=proxy_type, delay=delay, index=index, ) @@ -101,7 +96,7 @@ def kill_pure( self, spawner: str, proxy: str, - proxy_type: Union[str, ProxyType], + proxy_type: str, height: int, ext_index: int, ) -> Call: @@ -110,18 +105,17 @@ def kill_pure( Parameters: spawner: The SS58 address of the account that spawned the pure proxy. proxy: The SS58 address of the pure proxy account to kill. - proxy_type: The type of proxy permissions. Can be a string or ProxyType enum value. + proxy_type: The type of proxy permissions. height: The block height at which the pure proxy was created. ext_index: The extrinsic index at which the pure proxy was created. Returns: GenericCall instance. """ - proxy_type_str = ProxyType.normalize(proxy_type) return self.create_composed_call( spawner=spawner, proxy=proxy, - proxy_type=proxy_type_str, + proxy_type=proxy_type, height=height, ext_index=ext_index, ) @@ -129,7 +123,7 @@ def kill_pure( def proxy( self, real: str, - force_proxy_type: Optional[Union[str, ProxyType]], + force_proxy_type: Optional[str], call: "GenericCall", ) -> Call: """Returns GenericCall instance for Subtensor function Proxy.proxy. @@ -137,16 +131,15 @@ def proxy( Parameters: real: The SS58 address of the real account on whose behalf the call is being made. force_proxy_type: The type of proxy to use for the call. If None, any proxy type can be used. Otherwise, - must match one of the allowed proxy types. Can be a string or ProxyType enum value. + must match one of the allowed proxy types. call: The inner call to be executed on behalf of the real account. Returns: GenericCall instance. """ - force_proxy_type_str = ProxyType.normalize(force_proxy_type) if force_proxy_type is not None else None return self.create_composed_call( real=real, - force_proxy_type=force_proxy_type_str, + force_proxy_type=force_proxy_type, call=call, ) @@ -168,3 +161,67 @@ def announce( real=real, call_hash=call_hash, ) + + def proxy_announced( + self, + delegate: str, + real: str, + force_proxy_type: Optional[str], + call: "GenericCall", + ) -> Call: + """Returns GenericCall instance for Subtensor function Proxy.proxy_announced. + + Parameters: + delegate: The SS58 address of the delegate proxy account that made the announcement. + real: The SS58 address of the real account on whose behalf the call will be made. + force_proxy_type: The type of proxy to use for the call. If None, any proxy type can be used. Otherwise, + must match one of the allowed proxy types. + call: The inner call to be executed on behalf of the real account (must match the announced call_hash). + + Returns: + GenericCall instance. + """ + return self.create_composed_call( + delegate=delegate, + real=real, + force_proxy_type=force_proxy_type, + call=call, + ) + + def reject_announcement( + self, + delegate: str, + call_hash: str, + ) -> Call: + """Returns GenericCall instance for Subtensor function Proxy.reject_announcement. + + Parameters: + delegate: The SS58 address of the delegate proxy account whose announcement is being rejected. + call_hash: The hash of the call that was announced and is now being rejected. + + Returns: + GenericCall instance. + """ + return self.create_composed_call( + delegate=delegate, + call_hash=call_hash, + ) + + def remove_announcement( + self, + real: str, + call_hash: str, + ) -> Call: + """Returns GenericCall instance for Subtensor function Proxy.remove_announcement. + + Parameters: + real: The SS58 address of the real account on whose behalf the call was announced. + call_hash: The hash of the call that was announced and is now being removed. + + Returns: + GenericCall instance. + """ + return self.create_composed_call( + real=real, + call_hash=call_hash, + ) From 64d72a6d15d6a1fb212a1b3991daa084f10cb620 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Fri, 7 Nov 2025 11:44:42 -0800 Subject: [PATCH 11/62] update extrinsics structure --- bittensor/core/extrinsics/pallets/proxy.py | 3 +- bittensor/core/extrinsics/proxy.py | 271 ++++++++++++++++++--- 2 files changed, 245 insertions(+), 29 deletions(-) diff --git a/bittensor/core/extrinsics/pallets/proxy.py b/bittensor/core/extrinsics/pallets/proxy.py index 8a18f3cf75..a154e6797d 100644 --- a/bittensor/core/extrinsics/pallets/proxy.py +++ b/bittensor/core/extrinsics/pallets/proxy.py @@ -9,7 +9,8 @@ @dataclass class Proxy(_BasePallet): - """Factory class for creating GenericCall objects for Proxy pallet functions. + """ + Factory class for creating GenericCall objects for Proxy pallet functions. This class provides methods to create GenericCall instances for all Proxy pallet extrinsics. diff --git a/bittensor/core/extrinsics/proxy.py b/bittensor/core/extrinsics/proxy.py index b038bea830..42d40f6cdb 100644 --- a/bittensor/core/extrinsics/proxy.py +++ b/bittensor/core/extrinsics/proxy.py @@ -82,24 +82,26 @@ def add_proxy_extrinsic( return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) -def announce_extrinsic( +def remove_proxy_extrinsic( subtensor: "Subtensor", wallet: "Wallet", - real_ss58: str, - call_hash: str, + delegate_ss58: str, + proxy_type: Union[str, ProxyType], + delay: int, period: Optional[int] = None, raise_error: bool = False, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, ) -> ExtrinsicResponse: """ - Announces a future call that will be executed through a proxy. + Removes a proxy relationship. Parameters: subtensor: Subtensor instance with the connection to the chain. - wallet: Bittensor wallet object (should be the proxy account wallet). - real_ss58: The SS58 address of the real account on whose behalf the call will be made. - call_hash: The hash of the call that will be executed in the future. + wallet: Bittensor wallet object. + delegate_ss58: The SS58 address of the delegate proxy account to remove. + proxy_type: The type of proxy permissions to remove. Can be a string or ProxyType enum value. + delay: The number of blocks before the proxy removal takes effect. 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. @@ -116,15 +118,18 @@ def announce_extrinsic( ).success: return unlocked + proxy_type_str = ProxyType.normalize(proxy_type) + logging.debug( - f"Announcing proxy call: real=[blue]{real_ss58}[/blue], " - f"call_hash=[blue]{call_hash}[/blue] " + f"Removing proxy: delegate=[blue]{delegate_ss58}[/blue], " + f"type=[blue]{proxy_type_str}[/blue], delay=[blue]{delay}[/blue] " f"on [blue]{subtensor.network}[/blue]." ) - call = Proxy(subtensor).announce( - real=real_ss58, - call_hash=call_hash, + call = Proxy(subtensor).remove_proxy( + delegate=delegate_ss58, + proxy_type=proxy_type_str, + delay=delay, ) response = subtensor.sign_and_send_extrinsic( @@ -137,7 +142,7 @@ def announce_extrinsic( ) if response.success: - logging.debug("[green]Proxy call announced successfully.[/green]") + logging.debug("[green]Proxy removed successfully.[/green]") else: logging.error(f"[red]{response.message}[/red]") @@ -366,26 +371,32 @@ def proxy_extrinsic( return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) -def remove_proxy_extrinsic( +def proxy_announced_extrinsic( subtensor: "Subtensor", wallet: "Wallet", delegate_ss58: str, - proxy_type: Union[str, ProxyType], - delay: int, + real_ss58: str, + force_proxy_type: Optional[Union[str, ProxyType]], + call: "GenericCall", period: Optional[int] = None, raise_error: bool = False, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, ) -> ExtrinsicResponse: """ - Removes a proxy relationship. + Executes an announced call on behalf of the real account through a proxy. + + This extrinsic executes a call that was previously announced via `announce_extrinsic`. The call must match the + call_hash that was announced, and the delay period must have passed. Parameters: subtensor: Subtensor instance with the connection to the chain. - wallet: Bittensor wallet object. - delegate_ss58: The SS58 address of the delegate proxy account to remove. - proxy_type: The type of proxy permissions to remove. Can be a string or ProxyType enum value. - delay: The number of blocks before the proxy removal takes effect. + wallet: Bittensor wallet object (should be the proxy account wallet that made the announcement). + delegate_ss58: The SS58 address of the delegate proxy account that made the announcement. + real_ss58: The SS58 address of the real account on whose behalf the call will be made. + force_proxy_type: The type of proxy to use for the call. If None, any proxy type can be used. Otherwise, must + match one of the allowed proxy types. Can be a string or ProxyType enum value. + call: The inner call to be executed on behalf of the real account (must match the announced call_hash). 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. @@ -402,18 +413,86 @@ def remove_proxy_extrinsic( ).success: return unlocked - proxy_type_str = ProxyType.normalize(proxy_type) + force_proxy_type_str = ( + ProxyType.normalize(force_proxy_type) if force_proxy_type is not None else None + ) logging.debug( - f"Removing proxy: delegate=[blue]{delegate_ss58}[/blue], " - f"type=[blue]{proxy_type_str}[/blue], delay=[blue]{delay}[/blue] " + f"Executing announced proxy call: delegate=[blue]{delegate_ss58}[/blue], " + f"real=[blue]{real_ss58}[/blue], force_type=[blue]{force_proxy_type_str}[/blue] " f"on [blue]{subtensor.network}[/blue]." ) - call = Proxy(subtensor).remove_proxy( + proxy_call = Proxy(subtensor).proxy_announced( delegate=delegate_ss58, - proxy_type=proxy_type_str, - delay=delay, + real=real_ss58, + force_proxy_type=force_proxy_type_str, + call=call, + ) + + response = subtensor.sign_and_send_extrinsic( + call=proxy_call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + raise_error=raise_error, + ) + + if response.success: + logging.debug("[green]Announced proxy call executed successfully.[/green]") + else: + logging.error(f"[red]{response.message}[/red]") + + return response + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + +def announce_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + real_ss58: str, + call_hash: str, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> ExtrinsicResponse: + """ + Announces a future call that will be executed through a proxy. + + Parameters: + subtensor: Subtensor instance with the connection to the chain. + wallet: Bittensor wallet object (should be the proxy account wallet). + real_ss58: The SS58 address of the real account on whose behalf the call will be made. + call_hash: The hash of the call that will be executed in the future. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + logging.debug( + f"Announcing proxy call: real=[blue]{real_ss58}[/blue], " + f"call_hash=[blue]{call_hash}[/blue] " + f"on [blue]{subtensor.network}[/blue]." + ) + + call = Proxy(subtensor).announce( + real=real_ss58, + call_hash=call_hash, ) response = subtensor.sign_and_send_extrinsic( @@ -426,7 +505,143 @@ def remove_proxy_extrinsic( ) if response.success: - logging.debug("[green]Proxy removed successfully.[/green]") + logging.debug("[green]Proxy call announced successfully.[/green]") + else: + logging.error(f"[red]{response.message}[/red]") + + return response + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + +def reject_announcement_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + delegate_ss58: str, + call_hash: str, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> ExtrinsicResponse: + """ + Rejects an announcement made by a proxy delegate. + + This extrinsic allows the real account to reject an announcement made by a proxy delegate. This prevents the + announced call from being executed. + + Parameters: + subtensor: Subtensor instance with the connection to the chain. + wallet: Bittensor wallet object (should be the real account wallet). + delegate_ss58: The SS58 address of the delegate proxy account whose announcement is being rejected. + call_hash: The hash of the call that was announced and is now being rejected. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + logging.debug( + f"Rejecting announcement: delegate=[blue]{delegate_ss58}[/blue], " + f"call_hash=[blue]{call_hash}[/blue] " + f"on [blue]{subtensor.network}[/blue]." + ) + + call = Proxy(subtensor).reject_announcement( + delegate=delegate_ss58, + call_hash=call_hash, + ) + + response = 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, + ) + + if response.success: + logging.debug("[green]Announcement rejected successfully.[/green]") + else: + logging.error(f"[red]{response.message}[/red]") + + return response + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + +def remove_announcement_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + real_ss58: str, + call_hash: str, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> ExtrinsicResponse: + """ + Removes an announcement made by a proxy account. + + This extrinsic allows the proxy account to remove its own announcement before it is executed or rejected. This + frees up the announcement deposit. + + Parameters: + subtensor: Subtensor instance with the connection to the chain. + wallet: Bittensor wallet object (should be the proxy account wallet that made the announcement). + real_ss58: The SS58 address of the real account on whose behalf the call was announced. + call_hash: The hash of the call that was announced and is now being removed. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + logging.debug( + f"Removing announcement: real=[blue]{real_ss58}[/blue], " + f"call_hash=[blue]{call_hash}[/blue] " + f"on [blue]{subtensor.network}[/blue]." + ) + + call = Proxy(subtensor).remove_announcement( + real=real_ss58, + call_hash=call_hash, + ) + + response = 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, + ) + + if response.success: + logging.debug("[green]Announcement removed successfully.[/green]") else: logging.error(f"[red]{response.message}[/red]") From aff680eea8ce346ab4fd871c8b275f8ae5fbbae4 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Fri, 7 Nov 2025 11:50:55 -0800 Subject: [PATCH 12/62] add query methods --- bittensor/core/subtensor.py | 66 +++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 2a3d349513..07ba241d00 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -2225,6 +2225,72 @@ def get_parents( return [] + def get_proxies( + self, + real_account_ss58: str, + block: Optional[int] = None, + ) -> tuple[list[ProxyInfo], Balance]: + """Returns a list of proxies for the given account. + + Parameters: + real_account_ss58: SS58 address of the real (delegator) account. + block: The blockchain block number for the query. + + Returns: + The tuple containing a list of ProxiInfo objects and reserved deposit amount. + """ + block_hash = self.determine_block_hash(block) + query = self.substrate.query( + module="Proxy", + storage_function="Proxies", + params=[real_account_ss58], + block_hash=block_hash, + ) + + if query is None or query.value is None or not query.value[0][0]: + return [], Balance.from_rao(0) + + return ProxyInfo.from_query(query) + + def get_proxy_constants( + self, + constants: Optional[list[str]] = None, + as_dict: bool = False, + block: Optional[int] = None, + ) -> Union["ProxyConstants", dict]: + """ + Fetches runtime configuration constants from the `Proxy` pallet. + + If a list of constant names is provided, only those constants will be queried. + Otherwise, all known constants defined in `ProxyConstants.field_names()` are fetched. + + Parameters: + constants: A list of specific constant names to fetch from the pallet. If omitted, all constants from + `ProxyConstants` are queried. + as_dict: If True, returns the constants as a dictionary instead of a `ProxyConstants` object. + block: The blockchain block number for the query. + + Returns: + ProxyConstants: + A structured dataclass containing the retrieved values. Missing constants are returned as `None`. + """ + result = {} + const_names = constants or ProxyConstants.constants_names() + + for const_name in const_names: + query = self.query_constant( + module_name="Proxy", + constant_name=const_name, + block=block, + ) + + if query is not None: + result[const_name] = query.value + + proxy_constants = ProxyConstants.from_dict(result) + + return proxy_constants.to_dict() if as_dict else proxy_constants + def get_revealed_commitment( self, netuid: int, From 9f957a64a76185a878cab70127e44fa9c3c41e19 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Sat, 8 Nov 2025 03:42:24 -0800 Subject: [PATCH 13/62] add 2 more calls in pallet --- bittensor/core/extrinsics/pallets/proxy.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/bittensor/core/extrinsics/pallets/proxy.py b/bittensor/core/extrinsics/pallets/proxy.py index a154e6797d..09738ff173 100644 --- a/bittensor/core/extrinsics/pallets/proxy.py +++ b/bittensor/core/extrinsics/pallets/proxy.py @@ -71,6 +71,14 @@ def remove_proxy( delay=delay, ) + def remove_proxies(self) -> Call: + """Returns GenericCall instance for Proxy.remove_proxies. + + Returns: + GenericCall instance. + """ + return self.create_composed_call() + def create_pure( self, proxy_type: str, @@ -226,3 +234,11 @@ def remove_announcement( real=real, call_hash=call_hash, ) + + def poke_deposit(self) -> Call: + """Returns GenericCall instance for Proxy.poke_deposit. + + Returns: + GenericCall instance. + """ + return self.create_composed_call() From 1d0cc1882fdb5891e715ebdb50bc08d1f8cd2864 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Sat, 8 Nov 2025 03:42:33 -0800 Subject: [PATCH 14/62] add 2 more extrinsics --- bittensor/core/extrinsics/proxy.py | 170 ++++++++++++++++++++++++----- 1 file changed, 145 insertions(+), 25 deletions(-) diff --git a/bittensor/core/extrinsics/proxy.py b/bittensor/core/extrinsics/proxy.py index 42d40f6cdb..d2a2f40f86 100644 --- a/bittensor/core/extrinsics/proxy.py +++ b/bittensor/core/extrinsics/proxy.py @@ -152,6 +152,64 @@ def remove_proxy_extrinsic( return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) +def remove_proxies_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> ExtrinsicResponse: + """ + Removes all proxy relationships for the account. + + This removes all proxy relationships in a single call, which is more efficient than removing them one by one. The + deposit for all proxies will be returned. + + Parameters: + subtensor: Subtensor instance with the connection to the chain. + wallet: Bittensor wallet object (the account whose proxies will be removed). + period: The number of blocks during which the transaction will remain valid after it's submitted. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + logging.debug( + f"Removing all proxies for account [blue]{wallet.coldkey.ss58_address}[/blue] " + f"on [blue]{subtensor.network}[/blue]." + ) + + call = Proxy(subtensor).remove_proxies() + + response = 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, + ) + + if response.success: + logging.debug("[green]All proxies removed successfully.[/green]") + else: + logging.error(f"[red]{response.message}[/red]") + + return response + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + def create_pure_proxy_extrinsic( subtensor: "Subtensor", wallet: "Wallet", @@ -225,7 +283,6 @@ def create_pure_proxy_extrinsic( def kill_pure_proxy_extrinsic( subtensor: "Subtensor", wallet: "Wallet", - spawner_ss58: str, proxy_ss58: str, proxy_type: Union[str, ProxyType], height: int, @@ -240,8 +297,8 @@ def kill_pure_proxy_extrinsic( Parameters: subtensor: Subtensor instance with the connection to the chain. - wallet: Bittensor wallet object. - spawner_ss58: The SS58 address of the account that spawned the pure proxy. + wallet: Bittensor wallet object. The wallet.coldkey.ss58_address must be the spawner of the pure proxy (the + account that created it via create_pure_proxy_extrinsic). proxy_ss58: The SS58 address of the pure proxy account to kill. proxy_type: The type of proxy permissions. Can be a string or ProxyType enum value. height: The block height at which the pure proxy was created. @@ -265,13 +322,13 @@ def kill_pure_proxy_extrinsic( proxy_type_str = ProxyType.normalize(proxy_type) logging.debug( - f"Killing pure proxy: spawner=[blue]{spawner_ss58}[/blue], " + f"Killing pure proxy: spawner=[blue]{wallet.coldkey.ss58_address}[/blue], " f"proxy=[blue]{proxy_ss58}[/blue], type=[blue]{proxy_type_str}[/blue] " f"on [blue]{subtensor.network}[/blue]." ) call = Proxy(subtensor).kill_pure( - spawner=spawner_ss58, + spawner=wallet.coldkey.ss58_address, proxy=proxy_ss58, proxy_type=proxy_type_str, height=height, @@ -301,7 +358,7 @@ def kill_pure_proxy_extrinsic( def proxy_extrinsic( subtensor: "Subtensor", wallet: "Wallet", - real_ss58: str, + real_account_ss58: str, force_proxy_type: Optional[Union[str, ProxyType]], call: "GenericCall", period: Optional[int] = None, @@ -315,7 +372,7 @@ def proxy_extrinsic( Parameters: subtensor: Subtensor instance with the connection to the chain. wallet: Bittensor wallet object (should be the proxy account wallet). - real_ss58: The SS58 address of the real account on whose behalf the call is being made. + real_account_ss58: The SS58 address of the real account on whose behalf the call is being made. force_proxy_type: The type of proxy to use for the call. If None, any proxy type can be used. Otherwise, must match one of the allowed proxy types. Can be a string or ProxyType enum value. call: The inner call to be executed on behalf of the real account. @@ -336,17 +393,19 @@ def proxy_extrinsic( return unlocked force_proxy_type_str = ( - ProxyType.normalize(force_proxy_type) if force_proxy_type is not None else None + ProxyType.normalize(force_proxy_type) + if force_proxy_type is not None + else None ) logging.debug( - f"Executing proxy call: real=[blue]{real_ss58}[/blue], " + f"Executing proxy call: real=[blue]{real_account_ss58}[/blue], " f"force_type=[blue]{force_proxy_type_str}[/blue] " f"on [blue]{subtensor.network}[/blue]." ) proxy_call = Proxy(subtensor).proxy( - real=real_ss58, + real=real_account_ss58, force_proxy_type=force_proxy_type_str, call=call, ) @@ -375,7 +434,7 @@ def proxy_announced_extrinsic( subtensor: "Subtensor", wallet: "Wallet", delegate_ss58: str, - real_ss58: str, + real_account_ss58: str, force_proxy_type: Optional[Union[str, ProxyType]], call: "GenericCall", period: Optional[int] = None, @@ -393,7 +452,7 @@ def proxy_announced_extrinsic( subtensor: Subtensor instance with the connection to the chain. wallet: Bittensor wallet object (should be the proxy account wallet that made the announcement). delegate_ss58: The SS58 address of the delegate proxy account that made the announcement. - real_ss58: The SS58 address of the real account on whose behalf the call will be made. + real_account_ss58: The SS58 address of the real account on whose behalf the call will be made. force_proxy_type: The type of proxy to use for the call. If None, any proxy type can be used. Otherwise, must match one of the allowed proxy types. Can be a string or ProxyType enum value. call: The inner call to be executed on behalf of the real account (must match the announced call_hash). @@ -414,18 +473,20 @@ def proxy_announced_extrinsic( return unlocked force_proxy_type_str = ( - ProxyType.normalize(force_proxy_type) if force_proxy_type is not None else None + ProxyType.normalize(force_proxy_type) + if force_proxy_type is not None + else None ) logging.debug( f"Executing announced proxy call: delegate=[blue]{delegate_ss58}[/blue], " - f"real=[blue]{real_ss58}[/blue], force_type=[blue]{force_proxy_type_str}[/blue] " + f"real=[blue]{real_account_ss58}[/blue], force_type=[blue]{force_proxy_type_str}[/blue] " f"on [blue]{subtensor.network}[/blue]." ) proxy_call = Proxy(subtensor).proxy_announced( delegate=delegate_ss58, - real=real_ss58, + real=real_account_ss58, force_proxy_type=force_proxy_type_str, call=call, ) @@ -453,7 +514,7 @@ def proxy_announced_extrinsic( def announce_extrinsic( subtensor: "Subtensor", wallet: "Wallet", - real_ss58: str, + real_account_ss58: str, call_hash: str, period: Optional[int] = None, raise_error: bool = False, @@ -466,7 +527,7 @@ def announce_extrinsic( Parameters: subtensor: Subtensor instance with the connection to the chain. wallet: Bittensor wallet object (should be the proxy account wallet). - real_ss58: The SS58 address of the real account on whose behalf the call will be made. + real_account_ss58: The SS58 address of the real account on whose behalf the call will be made. call_hash: The hash of the call that will be executed in the future. 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 @@ -485,13 +546,13 @@ def announce_extrinsic( return unlocked logging.debug( - f"Announcing proxy call: real=[blue]{real_ss58}[/blue], " + f"Announcing proxy call: real=[blue]{real_account_ss58}[/blue], " f"call_hash=[blue]{call_hash}[/blue] " f"on [blue]{subtensor.network}[/blue]." ) call = Proxy(subtensor).announce( - real=real_ss58, + real=real_account_ss58, call_hash=call_hash, ) @@ -586,7 +647,7 @@ def reject_announcement_extrinsic( def remove_announcement_extrinsic( subtensor: "Subtensor", wallet: "Wallet", - real_ss58: str, + real_account_ss58: str, call_hash: str, period: Optional[int] = None, raise_error: bool = False, @@ -596,13 +657,13 @@ def remove_announcement_extrinsic( """ Removes an announcement made by a proxy account. - This extrinsic allows the proxy account to remove its own announcement before it is executed or rejected. This - frees up the announcement deposit. + This extrinsic allows the proxy account to remove its own announcement before it is executed or rejected. This frees + up the announcement deposit. Parameters: subtensor: Subtensor instance with the connection to the chain. wallet: Bittensor wallet object (should be the proxy account wallet that made the announcement). - real_ss58: The SS58 address of the real account on whose behalf the call was announced. + real_account_ss58: The SS58 address of the real account on whose behalf the call was announced. call_hash: The hash of the call that was announced and is now being removed. 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 @@ -621,13 +682,13 @@ def remove_announcement_extrinsic( return unlocked logging.debug( - f"Removing announcement: real=[blue]{real_ss58}[/blue], " + f"Removing announcement: real=[blue]{real_account_ss58}[/blue], " f"call_hash=[blue]{call_hash}[/blue] " f"on [blue]{subtensor.network}[/blue]." ) call = Proxy(subtensor).remove_announcement( - real=real_ss58, + real=real_account_ss58, call_hash=call_hash, ) @@ -649,3 +710,62 @@ def remove_announcement_extrinsic( except Exception as error: return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + +def poke_deposit_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> ExtrinsicResponse: + """ + Adjusts deposits made for proxies and announcements based on current values. + + This can be used by accounts to possibly lower their locked amount. The function automatically recalculates deposits + for both proxy relationships and announcements for the signing account. The transaction fee is waived if the deposit + amount has changed. + + Parameters: + subtensor: Subtensor instance with the connection to the chain. + wallet: Bittensor wallet object (the account whose deposits will be adjusted). + period: The number of blocks during which the transaction will remain valid after it's submitted. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + logging.debug( + f"Poking deposit for account [blue]{wallet.coldkey.ss58_address}[/blue] " + f"on [blue]{subtensor.network}[/blue]." + ) + + call = Proxy(subtensor).poke_deposit() + + response = 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, + ) + + if response.success: + logging.debug("[green]Deposit poked successfully.[/green]") + else: + logging.error(f"[red]{response.message}[/red]") + + return response + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) From 2a0c18c5a5594f300948f130a9f9d890430fc925 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Sat, 8 Nov 2025 03:45:19 -0800 Subject: [PATCH 15/62] ruff --- bittensor/core/extrinsics/proxy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor/core/extrinsics/proxy.py b/bittensor/core/extrinsics/proxy.py index d2a2f40f86..bfcc8b7cab 100644 --- a/bittensor/core/extrinsics/proxy.py +++ b/bittensor/core/extrinsics/proxy.py @@ -179,7 +179,7 @@ def remove_proxies_extrinsic( """ try: if not ( - unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) ).success: return unlocked @@ -740,7 +740,7 @@ def poke_deposit_extrinsic( """ try: if not ( - unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) ).success: return unlocked From 1bc0790c791511c5a48e40c4bbdde279b4383fcd Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Sat, 8 Nov 2025 03:45:42 -0800 Subject: [PATCH 16/62] update chain data --- bittensor/core/chain_data/__init__.py | 3 +- bittensor/core/chain_data/proxy.py | 225 +++++++++++++++++++++++--- 2 files changed, 202 insertions(+), 26 deletions(-) diff --git a/bittensor/core/chain_data/__init__.py b/bittensor/core/chain_data/__init__.py index eb16ed305c..b8d00b75fb 100644 --- a/bittensor/core/chain_data/__init__.py +++ b/bittensor/core/chain_data/__init__.py @@ -23,7 +23,7 @@ from .neuron_info_lite import NeuronInfoLite from .prometheus_info import PrometheusInfo from .proposal_vote_data import ProposalVoteData -from .proxy import ProxyConstants, ProxyInfo, ProxyType +from .proxy import ProxyConstants, ProxyInfo, ProxyType, ProxyAnnouncementInfo from .scheduled_coldkey_swap_info import ScheduledColdkeySwapInfo from .stake_info import StakeInfo from .sim_swap import SimSwapResult @@ -56,6 +56,7 @@ "ProposalCallData", "ProposalVoteData", "ProxyConstants", + "ProxyAnnouncementInfo", "ProxyInfo", "ProxyType", "ScheduledColdkeySwapInfo", diff --git a/bittensor/core/chain_data/proxy.py b/bittensor/core/chain_data/proxy.py index 8177e942a9..b910b82344 100644 --- a/bittensor/core/chain_data/proxy.py +++ b/bittensor/core/chain_data/proxy.py @@ -13,24 +13,94 @@ class ProxyType(str, Enum): These types define the permissions that a proxy account has when acting on behalf of the real account. Each type restricts what operations the proxy can perform. + Proxy Type Descriptions: + + Any: Allows the proxy to execute any call on behalf of the real account. This is the most permissive but least + secure proxy type. Use with caution. + + Owner: Allows the proxy to manage subnet identity and settings. Permitted operations include: + - AdminUtils calls (except sudo_set_sn_owner_hotkey) + - set_subnet_identity + - update_symbol + + NonCritical: Allows all operations except critical ones that could harm the account. Prohibited operations: + - dissolve_network + - root_register + - burned_register + - Sudo calls + + NonTransfer: Allows all operations except those involving token transfers. Prohibited operations: + - All Balances module calls + - transfer_stake + - schedule_swap_coldkey + - swap_coldkey + + NonFungible: Allows all operations except token-related operations and registrations. Prohibited operations: + - All Balances module calls + - All staking operations (add_stake, remove_stake, unstake_all, swap_stake, move_stake, transfer_stake) + - Registration operations (burned_register, root_register) + - Key swap operations (schedule_swap_coldkey, swap_coldkey, swap_hotkey) + + Staking: Allows only staking-related operations. Permitted operations: + - add_stake, add_stake_limit + - remove_stake, remove_stake_limit, remove_stake_full_limit + - unstake_all, unstake_all_alpha + - swap_stake, swap_stake_limit + - move_stake + + Registration: Allows only neuron registration operations. Permitted operations: + - burned_register + - register + + Transfer: Allows only token transfer operations. Permitted operations: + - transfer_keep_alive + - transfer_allow_death + - transfer_all + - transfer_stake + + SmallTransfer: Allows only small token transfers below a specific limit. Permitted operations: + - transfer_keep_alive (if value < SMALL_TRANSFER_LIMIT) + - transfer_allow_death (if value < SMALL_TRANSFER_LIMIT) + - transfer_stake (if alpha_amount < SMALL_TRANSFER_LIMIT) + + ChildKeys: Allows only child key management operations. Permitted operations: + - set_children + - set_childkey_take + + SudoUncheckedSetCode: Allows only runtime code updates. Permitted operations: + - sudo_unchecked_weight with inner call System::set_code + + SwapHotkey: Allows only hotkey swap operations. Permitted operations: + - swap_hotkey + + SubnetLeaseBeneficiary: Allows subnet management and configuration operations. Permitted operations: + - start_call + - Multiple AdminUtils.sudo_set_* calls for subnet parameters, network settings, weights, alpha values, etc. + + RootClaim: Allows only root claim operations. Permitted operations: + - claim_root + Note: The values match exactly with the ProxyType enum defined in the Subtensor runtime. Any changes to the runtime enum must be reflected here. + + Warning: + The permissions described above may change over time as the Subtensor runtime evolves. For the most up-to-date + and authoritative information about proxy type permissions, refer to the Subtensor source code at: + https://github.com/opentensor/subtensor/blob/main/runtime/src/lib.rs + Specifically, look for the `impl InstanceFilter for ProxyType` implementation which defines the + exact filtering logic for each proxy type. """ - any = "Any" + Any = "Any" Owner = "Owner" NonCritical = "NonCritical" NonTransfer = "NonTransfer" - Senate = "Senate" NonFungible = "NonFungible" - Triumvirate = "Triumvirate" - Governance = "Governance" Staking = "Staking" Registration = "Registration" Transfer = "Transfer" SmallTransfer = "SmallTransfer" - RootWeights = "RootWeights" ChildKeys = "ChildKeys" SudoUncheckedSetCode = "SudoUncheckedSetCode" SwapHotkey = "SwapHotkey" @@ -90,45 +160,150 @@ class ProxyInfo: Attributes: delegate: The SS58 address of the delegate proxy account that can act on behalf of the real account. - proxy_type: The type of proxy permissions granted to the delegate (e.g., "Any", "NonTransfer", "Governance", + proxy_type: The type of proxy permissions granted to the delegate (e.g., "Any", "NonTransfer", "ChildKeys", "Staking"). This determines what operations the delegate can perform. delay: The number of blocks that must pass before the proxy relationship becomes active. This delay provides a security mechanism allowing the real account to cancel the proxy if needed. """ + delegate: str proxy_type: str delay: int @classmethod - def from_dict(cls, data: dict): - """Returns a ProxyInfo object from proxy data.""" - return cls( - delegate=decode_account_id(data["delegate"]), - proxy_type=data["proxy_type"], - delay=data["delay"], - ) + def from_tuple(cls, data: tuple) -> list["ProxyInfo"]: + """Returns a list of ProxyInfo objects from a tuple of proxy data. - @classmethod - def from_tuple(cls, data: tuple): - """Returns a list of ProxyInfo objects from a tuple of proxy data.""" + Parameters: + data: Tuple of chain proxy data. + + Returns: + Tuple of ProxyInfo objects. + """ return [ cls( delegate=decode_account_id(proxy["delegate"]), - proxy_type=proxy["proxy_type"], + proxy_type=next(iter(proxy["proxy_type"].keys())), delay=proxy["delay"], ) for proxy in data ] @classmethod - def from_query(cls, query: Any): - """Returns a ProxyInfo object from a Substrate query.""" - try: - proxies = query.value[0][0] - balance = query.value[1] - return cls.from_tuple(proxies), Balance.from_rao(balance) - except IndexError: - return [], Balance.from_rao(0) + def from_query(cls, query: Any) -> tuple[list["ProxyInfo"], Balance]: + """ + Creates a list of ProxyInfo objects and deposit balance from a Substrate query result. + + Parameters: + query: Query result from Substrate containing proxy data structure. + + Returns: + Tuple containing: + - List of ProxyInfo objects representing all proxy relationships for the real account. + - Balance object representing the reserved deposit amount. + """ + # proxies data is always in that path + proxies = query.value[0][0] + # balance data is always in that path + balance = query.value[1] + return ( + (cls.from_tuple(proxies), Balance.from_rao(balance)) if proxies else tuple() + ) + + @classmethod + def from_query_map_record(cls, record: list) -> tuple[str, list["ProxyInfo"]]: + """ + Creates a dictionary mapping delegate addresses to their ProxyInfo lists from a query_map record. + + Processes a single record from a query_map call to the Proxy.Proxies storage function. Each record represents + one real account and its associated proxy/ies relationships. + + Parameters: + record: Data item from query_map records call to Proxies storage function. + + Returns: + Tuple containing: + - SS58 address of the real account (delegator). + - List of ProxyInfo objects representing all proxy relationships for this real account. + """ + # record[0] is the real account (key from storage) + # record[1] is the value containing proxies data + real_account_ss58 = decode_account_id(record[0]) + # list with proxies data is always in that path + proxy_data = cls.from_tuple(record[1].value[0][0]) + return real_account_ss58, proxy_data + + +@dataclass +class PureProxyInfo: + """ + Dataclass representing pure proxy information. + + Attributes: + spawner: The SS58 address of the account that spawned the pure proxy. + proxy: The SS58 address of the pure proxy account. + proxy_type: The type of proxy permissions. + delay: The number of blocks before the pure proxy can be used. + height: The block height at which the pure proxy was created. + ext_index: The extrinsic index at which the pure proxy was created. + """ + + spawner: str + proxy: str + proxy_type: str + delay: int + height: int + ext_index: int + + +@dataclass +class ProxyAnnouncementInfo: + """ + Dataclass representing proxy announcement information. + + This class contains information about a pending proxy announcement. An announcement allows a proxy account to + declare its intention to execute a call on behalf of the real account after a delay period. + + Attributes: + real: The SS58 address of the real account on whose behalf the call will be made. + call_hash: The hash of the call that will be executed in the future. + height: The block height at which the announcement was made. + """ + + real: str + call_hash: str + height: int + + @classmethod + def from_dict(cls, data: tuple) -> list["ProxyAnnouncementInfo"]: + """Returns a list of ProxyAnnouncementInfo objects from a tuple of announcement data. + + Parameters: + data: Tuple of announcements data. + + Returns: + Tuple of ProxyAnnouncementInfo objects. + """ + return [ + cls( + real=decode_account_id(next(iter(annt["real"]))), + call_hash="0x" + bytes(next(iter(annt["call_hash"]))).hex(), + height=annt["height"], + ) + for annt in data[0] + ] + + @classmethod + def from_query_map_record( + cls, record: tuple + ) -> tuple[str, list["ProxyAnnouncementInfo"]]: + """Returns a list of ProxyAnnouncementInfo objects from a tuple of announcements data.""" + # record[0] is the real account (key from storage) + # record[1] is the value containing announcements data + delegate = decode_account_id(record[0]) + # list with proxies data is always in that path + announcements_data = cls.from_dict(record[1].value[0]) + return delegate, announcements_data @dataclass From 20fefb21ffba4ba4171fe862a33c578b455eae84 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Sat, 8 Nov 2025 04:04:47 -0800 Subject: [PATCH 17/62] update SubtensorApi (less pain with utils) --- bittensor/extras/subtensor_api/__init__.py | 6 + bittensor/extras/subtensor_api/proxy.py | 25 + bittensor/extras/subtensor_api/utils.py | 522 +++++++++++---------- 3 files changed, 315 insertions(+), 238 deletions(-) create mode 100644 bittensor/extras/subtensor_api/proxy.py diff --git a/bittensor/extras/subtensor_api/__init__.py b/bittensor/extras/subtensor_api/__init__.py index 68dc1890ae..ab1cdd5f63 100644 --- a/bittensor/extras/subtensor_api/__init__.py +++ b/bittensor/extras/subtensor_api/__init__.py @@ -9,6 +9,7 @@ from .extrinsics import Extrinsics as _Extrinsics from .metagraphs import Metagraphs as _Metagraphs from .neurons import Neurons as _Neurons +from .proxy import Proxy as _Proxy from .queries import Queries as _Queries from .staking import Staking as _Staking from .subnets import Subnets as _Subnets @@ -240,6 +241,11 @@ def neurons(self, value): """Setter for neurons property.""" self._neurons = value + @property + def proxies(self): + """Property to access subtensor proxy methods.""" + return _Proxy(self.inner_subtensor) + @property def queries(self): """Property to access subtensor queries methods.""" diff --git a/bittensor/extras/subtensor_api/proxy.py b/bittensor/extras/subtensor_api/proxy.py new file mode 100644 index 0000000000..fcee4cf713 --- /dev/null +++ b/bittensor/extras/subtensor_api/proxy.py @@ -0,0 +1,25 @@ +from typing import Union +from bittensor.core.subtensor import Subtensor as _Subtensor +from bittensor.core.async_subtensor import AsyncSubtensor as _AsyncSubtensor + + +class Proxy: + """Class for managing proxy operations.""" + + def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): + self.add_proxy = subtensor.add_proxy + self.announce_proxy = subtensor.announce_proxy + self.create_pure_proxy = subtensor.create_pure_proxy + self.get_proxies = subtensor.get_proxies + self.get_proxies_for_real_account = subtensor.get_proxies_for_real_account + self.get_proxy_announcement = subtensor.get_proxy_announcement + self.get_proxy_announcements = subtensor.get_proxy_announcements + self.get_proxy_constants = subtensor.get_proxy_constants + self.kill_pure_proxy = subtensor.kill_pure_proxy + self.poke_deposit = subtensor.poke_deposit + self.proxy_announced = subtensor.proxy_announced + self.proxy = subtensor.proxy + self.reject_proxy_announcement = subtensor.reject_proxy_announcement + self.remove_proxies = subtensor.remove_proxies + self.remove_proxy = subtensor.remove_proxy + self.remove_proxy_announcement = subtensor.remove_proxy_announcement diff --git a/bittensor/extras/subtensor_api/utils.py b/bittensor/extras/subtensor_api/utils.py index 6d74f7d577..4ec47791a5 100644 --- a/bittensor/extras/subtensor_api/utils.py +++ b/bittensor/extras/subtensor_api/utils.py @@ -6,241 +6,287 @@ def add_legacy_methods(subtensor: "SubtensorApi"): """If SubtensorApi get `subtensor_fields=True` arguments, then all classic Subtensor fields added to root level.""" - subtensor.add_liquidity = subtensor.inner_subtensor.add_liquidity - subtensor.add_stake = subtensor.inner_subtensor.add_stake - subtensor.add_stake_multiple = subtensor.inner_subtensor.add_stake_multiple - subtensor.all_subnets = subtensor.inner_subtensor.all_subnets - subtensor.blocks_since_last_step = subtensor.inner_subtensor.blocks_since_last_step - subtensor.blocks_since_last_update = ( - subtensor.inner_subtensor.blocks_since_last_update - ) - subtensor.bonds = subtensor.inner_subtensor.bonds - subtensor.burned_register = subtensor.inner_subtensor.burned_register - subtensor.chain_endpoint = subtensor.inner_subtensor.chain_endpoint - subtensor.claim_root = subtensor.inner_subtensor.claim_root - subtensor.commit_reveal_enabled = subtensor.inner_subtensor.commit_reveal_enabled - subtensor.commit_weights = subtensor.inner_subtensor.commit_weights - subtensor.contribute_crowdloan = subtensor.inner_subtensor.contribute_crowdloan - subtensor.create_crowdloan = subtensor.inner_subtensor.create_crowdloan - subtensor.dissolve_crowdloan = subtensor.inner_subtensor.dissolve_crowdloan - subtensor.finalize_crowdloan = subtensor.inner_subtensor.finalize_crowdloan - subtensor.get_crowdloan_constants = ( - subtensor.inner_subtensor.get_crowdloan_constants - ) - subtensor.get_crowdloan_contributions = ( - subtensor.inner_subtensor.get_crowdloan_contributions - ) - subtensor.get_crowdloan_by_id = subtensor.inner_subtensor.get_crowdloan_by_id - subtensor.get_crowdloan_next_id = subtensor.inner_subtensor.get_crowdloan_next_id - subtensor.get_crowdloans = subtensor.inner_subtensor.get_crowdloans - subtensor.determine_block_hash = subtensor.inner_subtensor.determine_block_hash - subtensor.difficulty = subtensor.inner_subtensor.difficulty - subtensor.does_hotkey_exist = subtensor.inner_subtensor.does_hotkey_exist - subtensor.encode_params = subtensor.inner_subtensor.encode_params - subtensor.filter_netuids_by_registered_hotkeys = ( - subtensor.inner_subtensor.filter_netuids_by_registered_hotkeys - ) - subtensor.get_admin_freeze_window = ( - subtensor.inner_subtensor.get_admin_freeze_window - ) - subtensor.get_all_commitments = subtensor.inner_subtensor.get_all_commitments - subtensor.get_all_ema_tao_inflow = subtensor.inner_subtensor.get_all_ema_tao_inflow - subtensor.get_all_metagraphs_info = ( - subtensor.inner_subtensor.get_all_metagraphs_info - ) - subtensor.get_all_neuron_certificates = ( - subtensor.inner_subtensor.get_all_neuron_certificates - ) - subtensor.get_all_revealed_commitments = ( - 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 - subtensor.get_block_hash = subtensor.inner_subtensor.get_block_hash - subtensor.get_block_info = subtensor.inner_subtensor.get_block_info - subtensor.get_children = subtensor.inner_subtensor.get_children - subtensor.get_children_pending = subtensor.inner_subtensor.get_children_pending - subtensor.get_commitment = subtensor.inner_subtensor.get_commitment - subtensor.get_commitment_metadata = ( - subtensor.inner_subtensor.get_commitment_metadata - ) - subtensor.get_current_block = subtensor.inner_subtensor.get_current_block - subtensor.get_delegate_by_hotkey = subtensor.inner_subtensor.get_delegate_by_hotkey - subtensor.get_delegate_identities = ( - subtensor.inner_subtensor.get_delegate_identities - ) - subtensor.get_delegate_take = subtensor.inner_subtensor.get_delegate_take - subtensor.get_delegated = subtensor.inner_subtensor.get_delegated - subtensor.get_delegates = subtensor.inner_subtensor.get_delegates - subtensor.get_ema_tao_inflow = subtensor.inner_subtensor.get_ema_tao_inflow - 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 - subtensor.get_last_bonds_reset = subtensor.inner_subtensor.get_last_bonds_reset - subtensor.get_last_commitment_bonds_reset_block = ( - subtensor.inner_subtensor.get_last_commitment_bonds_reset_block - ) - subtensor.get_liquidity_list = subtensor.inner_subtensor.get_liquidity_list - subtensor.get_mechanism_count = subtensor.inner_subtensor.get_mechanism_count - subtensor.get_mechanism_emission_split = ( - subtensor.inner_subtensor.get_mechanism_emission_split - ) - subtensor.get_metagraph_info = subtensor.inner_subtensor.get_metagraph_info - subtensor.get_minimum_required_stake = ( - subtensor.inner_subtensor.get_minimum_required_stake - ) - subtensor.get_netuids_for_hotkey = subtensor.inner_subtensor.get_netuids_for_hotkey - subtensor.get_neuron_certificate = subtensor.inner_subtensor.get_neuron_certificate - subtensor.get_neuron_for_pubkey_and_subnet = ( - subtensor.inner_subtensor.get_neuron_for_pubkey_and_subnet - ) - subtensor.get_next_epoch_start_block = ( - subtensor.inner_subtensor.get_next_epoch_start_block - ) - subtensor.get_owned_hotkeys = subtensor.inner_subtensor.get_owned_hotkeys - subtensor.get_parents = subtensor.inner_subtensor.get_parents - subtensor.get_revealed_commitment = ( - subtensor.inner_subtensor.get_revealed_commitment - ) - subtensor.get_revealed_commitment_by_hotkey = ( - subtensor.inner_subtensor.get_revealed_commitment_by_hotkey - ) - subtensor.get_root_claim_type = subtensor.inner_subtensor.get_root_claim_type - subtensor.get_root_claimable_all_rates = ( - subtensor.inner_subtensor.get_root_claimable_all_rates - ) - subtensor.get_root_claimable_rate = ( - subtensor.inner_subtensor.get_root_claimable_rate - ) - subtensor.get_root_claimable_stake = ( - subtensor.inner_subtensor.get_root_claimable_stake - ) - subtensor.get_root_claimed = subtensor.inner_subtensor.get_root_claimed - subtensor.get_stake = subtensor.inner_subtensor.get_stake - subtensor.get_stake_add_fee = subtensor.inner_subtensor.get_stake_add_fee - subtensor.get_stake_for_coldkey_and_hotkey = ( - subtensor.inner_subtensor.get_stake_for_coldkey_and_hotkey - ) - subtensor.get_stake_for_hotkey = subtensor.inner_subtensor.get_stake_for_hotkey - subtensor.get_stake_info_for_coldkey = ( - subtensor.inner_subtensor.get_stake_info_for_coldkey - ) - subtensor.get_stake_movement_fee = subtensor.inner_subtensor.get_stake_movement_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 = ( - subtensor.inner_subtensor.get_subnet_hyperparameters - ) - subtensor.get_subnet_info = subtensor.inner_subtensor.get_subnet_info - subtensor.get_subnet_owner_hotkey = ( - subtensor.inner_subtensor.get_subnet_owner_hotkey - ) - subtensor.get_subnet_price = subtensor.inner_subtensor.get_subnet_price - subtensor.get_subnet_prices = subtensor.inner_subtensor.get_subnet_prices - subtensor.get_subnet_reveal_period_epochs = ( - subtensor.inner_subtensor.get_subnet_reveal_period_epochs - ) - subtensor.get_subnet_validator_permits = ( - subtensor.inner_subtensor.get_subnet_validator_permits - ) - subtensor.get_timelocked_weight_commits = ( - subtensor.inner_subtensor.get_timelocked_weight_commits - ) - subtensor.get_timestamp = subtensor.inner_subtensor.get_timestamp - subtensor.get_total_subnets = subtensor.inner_subtensor.get_total_subnets - subtensor.get_transfer_fee = subtensor.inner_subtensor.get_transfer_fee - subtensor.get_uid_for_hotkey_on_subnet = ( - subtensor.inner_subtensor.get_uid_for_hotkey_on_subnet - ) - subtensor.get_unstake_fee = subtensor.inner_subtensor.get_unstake_fee - subtensor.get_vote_data = subtensor.inner_subtensor.get_vote_data - subtensor.immunity_period = subtensor.inner_subtensor.immunity_period - subtensor.is_fast_blocks = subtensor.inner_subtensor.is_fast_blocks - subtensor.is_hotkey_delegate = subtensor.inner_subtensor.is_hotkey_delegate - subtensor.is_hotkey_registered = subtensor.inner_subtensor.is_hotkey_registered - subtensor.is_hotkey_registered_any = ( - subtensor.inner_subtensor.is_hotkey_registered_any - ) - subtensor.is_hotkey_registered_on_subnet = ( - subtensor.inner_subtensor.is_hotkey_registered_on_subnet - ) - subtensor.is_in_admin_freeze_window = ( - subtensor.inner_subtensor.is_in_admin_freeze_window - ) - subtensor.is_subnet_active = subtensor.inner_subtensor.is_subnet_active - subtensor.last_drand_round = subtensor.inner_subtensor.last_drand_round - subtensor.log_verbose = subtensor.inner_subtensor.log_verbose - subtensor.max_weight_limit = subtensor.inner_subtensor.max_weight_limit - subtensor.metagraph = subtensor.inner_subtensor.metagraph - subtensor.min_allowed_weights = subtensor.inner_subtensor.min_allowed_weights - subtensor.modify_liquidity = subtensor.inner_subtensor.modify_liquidity - subtensor.move_stake = subtensor.inner_subtensor.move_stake - subtensor.neuron_for_uid = subtensor.inner_subtensor.neuron_for_uid - subtensor.neurons = subtensor.inner_subtensor.neurons - subtensor.neurons_lite = subtensor.inner_subtensor.neurons_lite - subtensor.network = subtensor.inner_subtensor.network - subtensor.query_constant = subtensor.inner_subtensor.query_constant - subtensor.query_identity = subtensor.inner_subtensor.query_identity - subtensor.query_map = subtensor.inner_subtensor.query_map - subtensor.query_map_subtensor = subtensor.inner_subtensor.query_map_subtensor - subtensor.query_module = subtensor.inner_subtensor.query_module - subtensor.query_runtime_api = subtensor.inner_subtensor.query_runtime_api - subtensor.query_subtensor = subtensor.inner_subtensor.query_subtensor - subtensor.recycle = subtensor.inner_subtensor.recycle - subtensor.refund_crowdloan = subtensor.inner_subtensor.refund_crowdloan - subtensor.register = subtensor.inner_subtensor.register - subtensor.register_subnet = subtensor.inner_subtensor.register_subnet - subtensor.remove_liquidity = subtensor.inner_subtensor.remove_liquidity - subtensor.reveal_weights = subtensor.inner_subtensor.reveal_weights - subtensor.root_register = subtensor.inner_subtensor.root_register - subtensor.root_set_pending_childkey_cooldown = ( - subtensor.inner_subtensor.root_set_pending_childkey_cooldown - ) - subtensor.serve_axon = subtensor.inner_subtensor.serve_axon - subtensor.set_auto_stake = subtensor.inner_subtensor.set_auto_stake - subtensor.set_children = subtensor.inner_subtensor.set_children - subtensor.set_commitment = subtensor.inner_subtensor.set_commitment - subtensor.set_delegate_take = subtensor.inner_subtensor.set_delegate_take - subtensor.set_reveal_commitment = subtensor.inner_subtensor.set_reveal_commitment - subtensor.set_root_claim_type = subtensor.inner_subtensor.set_root_claim_type - subtensor.set_subnet_identity = subtensor.inner_subtensor.set_subnet_identity - subtensor.set_weights = subtensor.inner_subtensor.set_weights - subtensor.setup_config = subtensor.inner_subtensor.setup_config - 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 - subtensor.subnet_exists = subtensor.inner_subtensor.subnet_exists - subtensor.subnetwork_n = subtensor.inner_subtensor.subnetwork_n - subtensor.substrate = subtensor.inner_subtensor.substrate - subtensor.swap_stake = subtensor.inner_subtensor.swap_stake - subtensor.tempo = subtensor.inner_subtensor.tempo - subtensor.toggle_user_liquidity = subtensor.inner_subtensor.toggle_user_liquidity - subtensor.transfer = subtensor.inner_subtensor.transfer - subtensor.transfer_stake = subtensor.inner_subtensor.transfer_stake - subtensor.tx_rate_limit = subtensor.inner_subtensor.tx_rate_limit - subtensor.unstake = subtensor.inner_subtensor.unstake - subtensor.unstake_all = subtensor.inner_subtensor.unstake_all - subtensor.unstake_multiple = subtensor.inner_subtensor.unstake_multiple - subtensor.update_cap_crowdloan = subtensor.inner_subtensor.update_cap_crowdloan - subtensor.update_end_crowdloan = subtensor.inner_subtensor.update_end_crowdloan - subtensor.update_min_contribution_crowdloan = ( - subtensor.inner_subtensor.update_min_contribution_crowdloan - ) - 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 - subtensor.withdraw_crowdloan = subtensor.inner_subtensor.withdraw_crowdloan + # Attributes that should NOT be dynamically added (manually defined in SubtensorApi.__init__) + EXCLUDED_ATTRIBUTES = { + # Internal attributes + "inner_subtensor", + "initialize", + } + + # Get all attributes from inner_subtensor + for attr_name in dir(subtensor.inner_subtensor): + # Skip private attributes, special methods, and excluded attributes + if attr_name.startswith("_") or attr_name in EXCLUDED_ATTRIBUTES: + continue + + # Check if attribute already exists in subtensor (this automatically excludes + # all properties like block, chain, commitments, etc. and other defined attributes) + if hasattr(subtensor, attr_name): + continue + + # Get the attribute from inner_subtensor and add it + try: + attr_value = getattr(subtensor.inner_subtensor, attr_name) + setattr(subtensor, attr_name, attr_value) + except (AttributeError, TypeError): + # Skip if attribute cannot be accessed or set + continue + + +# def add_legacy_methods(subtensor: "SubtensorApi"): +# """If SubtensorApi get `subtensor_fields=True` arguments, then all classic Subtensor fields added to root level.""" +# subtensor.add_liquidity = subtensor.inner_subtensor.add_liquidity +# subtensor.add_stake = subtensor.inner_subtensor.add_stake +# subtensor.add_stake_multiple = subtensor.inner_subtensor.add_stake_multiple +# subtensor.all_subnets = subtensor.inner_subtensor.all_subnets +# subtensor.blocks_since_last_step = subtensor.inner_subtensor.blocks_since_last_step +# subtensor.blocks_since_last_update = ( +# subtensor.inner_subtensor.blocks_since_last_update +# ) +# subtensor.bonds = subtensor.inner_subtensor.bonds +# subtensor.burned_register = subtensor.inner_subtensor.burned_register +# subtensor.chain_endpoint = subtensor.inner_subtensor.chain_endpoint +# subtensor.claim_root = subtensor.inner_subtensor.claim_root +# subtensor.commit_reveal_enabled = subtensor.inner_subtensor.commit_reveal_enabled +# subtensor.commit_weights = subtensor.inner_subtensor.commit_weights +# subtensor.contribute_crowdloan = subtensor.inner_subtensor.contribute_crowdloan +# subtensor.create_crowdloan = subtensor.inner_subtensor.create_crowdloan +# subtensor.dissolve_crowdloan = subtensor.inner_subtensor.dissolve_crowdloan +# subtensor.finalize_crowdloan = subtensor.inner_subtensor.finalize_crowdloan +# subtensor.get_crowdloan_constants = ( +# subtensor.inner_subtensor.get_crowdloan_constants +# ) +# subtensor.get_crowdloan_contributions = ( +# subtensor.inner_subtensor.get_crowdloan_contributions +# ) +# subtensor.get_crowdloan_by_id = subtensor.inner_subtensor.get_crowdloan_by_id +# subtensor.get_crowdloan_next_id = subtensor.inner_subtensor.get_crowdloan_next_id +# subtensor.get_crowdloans = subtensor.inner_subtensor.get_crowdloans +# subtensor.determine_block_hash = subtensor.inner_subtensor.determine_block_hash +# subtensor.difficulty = subtensor.inner_subtensor.difficulty +# subtensor.does_hotkey_exist = subtensor.inner_subtensor.does_hotkey_exist +# subtensor.encode_params = subtensor.inner_subtensor.encode_params +# subtensor.filter_netuids_by_registered_hotkeys = ( +# subtensor.inner_subtensor.filter_netuids_by_registered_hotkeys +# ) +# subtensor.get_admin_freeze_window = ( +# subtensor.inner_subtensor.get_admin_freeze_window +# ) +# subtensor.get_all_commitments = subtensor.inner_subtensor.get_all_commitments +# subtensor.get_all_ema_tao_inflow = subtensor.inner_subtensor.get_all_ema_tao_inflow +# subtensor.get_all_metagraphs_info = ( +# subtensor.inner_subtensor.get_all_metagraphs_info +# ) +# subtensor.get_all_neuron_certificates = ( +# subtensor.inner_subtensor.get_all_neuron_certificates +# ) +# subtensor.get_all_revealed_commitments = ( +# 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 +# subtensor.get_block_hash = subtensor.inner_subtensor.get_block_hash +# subtensor.get_block_info = subtensor.inner_subtensor.get_block_info +# subtensor.get_children = subtensor.inner_subtensor.get_children +# subtensor.get_children_pending = subtensor.inner_subtensor.get_children_pending +# subtensor.get_commitment = subtensor.inner_subtensor.get_commitment +# subtensor.get_commitment_metadata = ( +# subtensor.inner_subtensor.get_commitment_metadata +# ) +# subtensor.get_current_block = subtensor.inner_subtensor.get_current_block +# subtensor.get_delegate_by_hotkey = subtensor.inner_subtensor.get_delegate_by_hotkey +# subtensor.get_delegate_identities = ( +# subtensor.inner_subtensor.get_delegate_identities +# ) +# subtensor.get_delegate_take = subtensor.inner_subtensor.get_delegate_take +# subtensor.get_delegated = subtensor.inner_subtensor.get_delegated +# subtensor.get_delegates = subtensor.inner_subtensor.get_delegates +# subtensor.get_ema_tao_inflow = subtensor.inner_subtensor.get_ema_tao_inflow +# 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 +# subtensor.get_last_bonds_reset = subtensor.inner_subtensor.get_last_bonds_reset +# subtensor.get_last_commitment_bonds_reset_block = ( +# subtensor.inner_subtensor.get_last_commitment_bonds_reset_block +# ) +# subtensor.get_liquidity_list = subtensor.inner_subtensor.get_liquidity_list +# subtensor.get_mechanism_count = subtensor.inner_subtensor.get_mechanism_count +# subtensor.get_mechanism_emission_split = ( +# subtensor.inner_subtensor.get_mechanism_emission_split +# ) +# subtensor.get_metagraph_info = subtensor.inner_subtensor.get_metagraph_info +# subtensor.get_minimum_required_stake = ( +# subtensor.inner_subtensor.get_minimum_required_stake +# ) +# subtensor.get_netuids_for_hotkey = subtensor.inner_subtensor.get_netuids_for_hotkey +# subtensor.get_neuron_certificate = subtensor.inner_subtensor.get_neuron_certificate +# subtensor.get_neuron_for_pubkey_and_subnet = ( +# subtensor.inner_subtensor.get_neuron_for_pubkey_and_subnet +# ) +# subtensor.get_next_epoch_start_block = ( +# subtensor.inner_subtensor.get_next_epoch_start_block +# ) +# subtensor.get_owned_hotkeys = subtensor.inner_subtensor.get_owned_hotkeys +# subtensor.get_parents = subtensor.inner_subtensor.get_parents +# subtensor.get_revealed_commitment = ( +# subtensor.inner_subtensor.get_revealed_commitment +# ) +# subtensor.get_revealed_commitment_by_hotkey = ( +# subtensor.inner_subtensor.get_revealed_commitment_by_hotkey +# ) +# subtensor.get_root_claim_type = subtensor.inner_subtensor.get_root_claim_type +# subtensor.get_root_claimable_all_rates = ( +# subtensor.inner_subtensor.get_root_claimable_all_rates +# ) +# subtensor.get_root_claimable_rate = ( +# subtensor.inner_subtensor.get_root_claimable_rate +# ) +# subtensor.get_root_claimable_stake = ( +# subtensor.inner_subtensor.get_root_claimable_stake +# ) +# subtensor.get_root_claimed = subtensor.inner_subtensor.get_root_claimed +# subtensor.get_stake = subtensor.inner_subtensor.get_stake +# subtensor.get_stake_add_fee = subtensor.inner_subtensor.get_stake_add_fee +# subtensor.get_stake_for_coldkey_and_hotkey = ( +# subtensor.inner_subtensor.get_stake_for_coldkey_and_hotkey +# ) +# subtensor.get_stake_for_hotkey = subtensor.inner_subtensor.get_stake_for_hotkey +# subtensor.get_stake_info_for_coldkey = ( +# subtensor.inner_subtensor.get_stake_info_for_coldkey +# ) +# subtensor.get_stake_movement_fee = subtensor.inner_subtensor.get_stake_movement_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 = ( +# subtensor.inner_subtensor.get_subnet_hyperparameters +# ) +# subtensor.get_subnet_info = subtensor.inner_subtensor.get_subnet_info +# subtensor.get_subnet_owner_hotkey = ( +# subtensor.inner_subtensor.get_subnet_owner_hotkey +# ) +# subtensor.get_subnet_price = subtensor.inner_subtensor.get_subnet_price +# subtensor.get_subnet_prices = subtensor.inner_subtensor.get_subnet_prices +# subtensor.get_subnet_reveal_period_epochs = ( +# subtensor.inner_subtensor.get_subnet_reveal_period_epochs +# ) +# subtensor.get_subnet_validator_permits = ( +# subtensor.inner_subtensor.get_subnet_validator_permits +# ) +# subtensor.get_timelocked_weight_commits = ( +# subtensor.inner_subtensor.get_timelocked_weight_commits +# ) +# subtensor.get_timestamp = subtensor.inner_subtensor.get_timestamp +# subtensor.get_total_subnets = subtensor.inner_subtensor.get_total_subnets +# subtensor.get_transfer_fee = subtensor.inner_subtensor.get_transfer_fee +# subtensor.get_uid_for_hotkey_on_subnet = ( +# subtensor.inner_subtensor.get_uid_for_hotkey_on_subnet +# ) +# subtensor.get_unstake_fee = subtensor.inner_subtensor.get_unstake_fee +# subtensor.get_vote_data = subtensor.inner_subtensor.get_vote_data +# subtensor.immunity_period = subtensor.inner_subtensor.immunity_period +# subtensor.is_fast_blocks = subtensor.inner_subtensor.is_fast_blocks +# subtensor.is_hotkey_delegate = subtensor.inner_subtensor.is_hotkey_delegate +# subtensor.is_hotkey_registered = subtensor.inner_subtensor.is_hotkey_registered +# subtensor.is_hotkey_registered_any = ( +# subtensor.inner_subtensor.is_hotkey_registered_any +# ) +# subtensor.is_hotkey_registered_on_subnet = ( +# subtensor.inner_subtensor.is_hotkey_registered_on_subnet +# ) +# subtensor.is_in_admin_freeze_window = ( +# subtensor.inner_subtensor.is_in_admin_freeze_window +# ) +# subtensor.is_subnet_active = subtensor.inner_subtensor.is_subnet_active +# subtensor.last_drand_round = subtensor.inner_subtensor.last_drand_round +# subtensor.log_verbose = subtensor.inner_subtensor.log_verbose +# subtensor.max_weight_limit = subtensor.inner_subtensor.max_weight_limit +# subtensor.metagraph = subtensor.inner_subtensor.metagraph +# subtensor.min_allowed_weights = subtensor.inner_subtensor.min_allowed_weights +# subtensor.modify_liquidity = subtensor.inner_subtensor.modify_liquidity +# subtensor.move_stake = subtensor.inner_subtensor.move_stake +# subtensor.neuron_for_uid = subtensor.inner_subtensor.neuron_for_uid +# subtensor.neurons = subtensor.inner_subtensor.neurons +# subtensor.neurons_lite = subtensor.inner_subtensor.neurons_lite +# subtensor.network = subtensor.inner_subtensor.network +# subtensor.query_constant = subtensor.inner_subtensor.query_constant +# subtensor.query_identity = subtensor.inner_subtensor.query_identity +# subtensor.query_map = subtensor.inner_subtensor.query_map +# subtensor.query_map_subtensor = subtensor.inner_subtensor.query_map_subtensor +# subtensor.query_module = subtensor.inner_subtensor.query_module +# subtensor.query_runtime_api = subtensor.inner_subtensor.query_runtime_api +# subtensor.query_subtensor = subtensor.inner_subtensor.query_subtensor +# subtensor.recycle = subtensor.inner_subtensor.recycle +# subtensor.refund_crowdloan = subtensor.inner_subtensor.refund_crowdloan +# subtensor.register = subtensor.inner_subtensor.register +# subtensor.register_subnet = subtensor.inner_subtensor.register_subnet +# subtensor.remove_liquidity = subtensor.inner_subtensor.remove_liquidity +# subtensor.reveal_weights = subtensor.inner_subtensor.reveal_weights +# subtensor.root_register = subtensor.inner_subtensor.root_register +# subtensor.root_set_pending_childkey_cooldown = ( +# subtensor.inner_subtensor.root_set_pending_childkey_cooldown +# ) +# subtensor.serve_axon = subtensor.inner_subtensor.serve_axon +# subtensor.set_auto_stake = subtensor.inner_subtensor.set_auto_stake +# subtensor.set_children = subtensor.inner_subtensor.set_children +# subtensor.set_commitment = subtensor.inner_subtensor.set_commitment +# subtensor.set_delegate_take = subtensor.inner_subtensor.set_delegate_take +# subtensor.set_reveal_commitment = subtensor.inner_subtensor.set_reveal_commitment +# subtensor.set_root_claim_type = subtensor.inner_subtensor.set_root_claim_type +# subtensor.set_subnet_identity = subtensor.inner_subtensor.set_subnet_identity +# subtensor.set_weights = subtensor.inner_subtensor.set_weights +# subtensor.setup_config = subtensor.inner_subtensor.setup_config +# 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 +# subtensor.subnet_exists = subtensor.inner_subtensor.subnet_exists +# subtensor.subnetwork_n = subtensor.inner_subtensor.subnetwork_n +# subtensor.substrate = subtensor.inner_subtensor.substrate +# subtensor.swap_stake = subtensor.inner_subtensor.swap_stake +# subtensor.tempo = subtensor.inner_subtensor.tempo +# subtensor.toggle_user_liquidity = subtensor.inner_subtensor.toggle_user_liquidity +# subtensor.transfer = subtensor.inner_subtensor.transfer +# subtensor.transfer_stake = subtensor.inner_subtensor.transfer_stake +# subtensor.tx_rate_limit = subtensor.inner_subtensor.tx_rate_limit +# subtensor.unstake = subtensor.inner_subtensor.unstake +# subtensor.unstake_all = subtensor.inner_subtensor.unstake_all +# subtensor.unstake_multiple = subtensor.inner_subtensor.unstake_multiple +# subtensor.update_cap_crowdloan = subtensor.inner_subtensor.update_cap_crowdloan +# subtensor.update_end_crowdloan = subtensor.inner_subtensor.update_end_crowdloan +# subtensor.update_min_contribution_crowdloan = ( +# subtensor.inner_subtensor.update_min_contribution_crowdloan +# ) +# 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 +# subtensor.withdraw_crowdloan = subtensor.inner_subtensor.withdraw_crowdloan +# +# subtensor.add_proxy = subtensor.inner_subtensor.add_proxy +# subtensor.announce_proxy = subtensor.inner_subtensor.announce_proxy +# subtensor.create_pure_proxy = subtensor.inner_subtensor.create_pure_proxy +# subtensor.get_proxies = subtensor.inner_subtensor.get_proxies +# subtensor.get_proxies_for_real_account = subtensor.inner_subtensor.get_proxies_for_real_account +# subtensor.get_proxy_announcement = subtensor.inner_subtensor.get_proxy_announcement +# subtensor.get_proxy_announcements = subtensor.inner_subtensor.get_proxy_announcements +# subtensor.get_proxy_constants = subtensor.inner_subtensor.get_proxy_constants +# subtensor.kill_pure_proxy = subtensor.inner_subtensor.kill_pure_proxy +# subtensor.poke_deposit = subtensor.inner_subtensor.poke_deposit +# subtensor.proxy_announced = subtensor.inner_subtensor.proxy_announced +# subtensor.proxy = subtensor.inner_subtensor.proxy +# subtensor.reject_proxy_announcement = subtensor.inner_subtensor.reject_proxy_announcement +# subtensor.remove_proxies = subtensor.inner_subtensor.remove_proxies +# subtensor.remove_proxy = subtensor.inner_subtensor.remove_proxy +# subtensor.remove_proxy_announcement = subtensor.inner_subtensor.remove_proxy_announcement From 28fcf401b7c64530534d95fbc1d151b614ea7eb6 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Sat, 8 Nov 2025 06:15:50 -0800 Subject: [PATCH 18/62] update chain data --- bittensor/core/chain_data/proxy.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/bittensor/core/chain_data/proxy.py b/bittensor/core/chain_data/proxy.py index b910b82344..cd68771e38 100644 --- a/bittensor/core/chain_data/proxy.py +++ b/bittensor/core/chain_data/proxy.py @@ -234,28 +234,6 @@ def from_query_map_record(cls, record: list) -> tuple[str, list["ProxyInfo"]]: return real_account_ss58, proxy_data -@dataclass -class PureProxyInfo: - """ - Dataclass representing pure proxy information. - - Attributes: - spawner: The SS58 address of the account that spawned the pure proxy. - proxy: The SS58 address of the pure proxy account. - proxy_type: The type of proxy permissions. - delay: The number of blocks before the pure proxy can be used. - height: The block height at which the pure proxy was created. - ext_index: The extrinsic index at which the pure proxy was created. - """ - - spawner: str - proxy: str - proxy_type: str - delay: int - height: int - ext_index: int - - @dataclass class ProxyAnnouncementInfo: """ From ec31ca11b82f22340934d78480ea2edcb2334be9 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Sat, 8 Nov 2025 06:15:57 -0800 Subject: [PATCH 19/62] update pallet --- bittensor/core/extrinsics/pallets/proxy.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor/core/extrinsics/pallets/proxy.py b/bittensor/core/extrinsics/pallets/proxy.py index 09738ff173..fbb73c441b 100644 --- a/bittensor/core/extrinsics/pallets/proxy.py +++ b/bittensor/core/extrinsics/pallets/proxy.py @@ -104,8 +104,8 @@ def create_pure( def kill_pure( self, spawner: str, - proxy: str, proxy_type: str, + index: int, height: int, ext_index: int, ) -> Call: @@ -113,8 +113,8 @@ def kill_pure( Parameters: spawner: The SS58 address of the account that spawned the pure proxy. - proxy: The SS58 address of the pure proxy account to kill. proxy_type: The type of proxy permissions. + index: The disambiguation index originally passed to `create_pure`. height: The block height at which the pure proxy was created. ext_index: The extrinsic index at which the pure proxy was created. @@ -123,8 +123,8 @@ def kill_pure( """ return self.create_composed_call( spawner=spawner, - proxy=proxy, proxy_type=proxy_type, + index=index, height=height, ext_index=ext_index, ) From 18ce7eaca740b8fc651396e02f04ea00b3a04eef Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Sat, 8 Nov 2025 06:16:11 -0800 Subject: [PATCH 20/62] update extrinsics --- bittensor/core/extrinsics/proxy.py | 38 ++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/bittensor/core/extrinsics/proxy.py b/bittensor/core/extrinsics/proxy.py index bfcc8b7cab..8cb0a5262c 100644 --- a/bittensor/core/extrinsics/proxy.py +++ b/bittensor/core/extrinsics/proxy.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING, Optional, Union -from bittensor.core.extrinsics.pallets import Proxy from bittensor.core.chain_data.proxy import ProxyType +from bittensor.core.extrinsics.pallets import Proxy from bittensor.core.types import ExtrinsicResponse from bittensor.utils.btlogging import logging @@ -271,6 +271,24 @@ def create_pure_proxy_extrinsic( if response.success: logging.debug("[green]Pure proxy created successfully.[/green]") + + # Extract pure proxy address from PureCreated triggered event + for event in response.extrinsic_receipt.triggered_events: + if event.get("event_id") == "PureCreated": + # Event structure: PureProxyCreated { disambiguation_index, proxy_type, pure, who } + attributes = event.get("attributes", []) + if attributes: + response.data = { + "pure_account": attributes.get("pure"), + "spawner": attributes.get("who"), + "proxy_type": attributes.get("proxy_type"), + "index": attributes.get("disambiguation_index"), + "height": subtensor.substrate.get_block_number( + response.extrinsic_receipt.block_hash + ), + "ext_index": response.extrinsic_receipt.extrinsic_idx, + } + break else: logging.error(f"[red]{response.message}[/red]") @@ -283,8 +301,9 @@ def create_pure_proxy_extrinsic( def kill_pure_proxy_extrinsic( subtensor: "Subtensor", wallet: "Wallet", - proxy_ss58: str, - proxy_type: Union[str, ProxyType], + spawner: str, + proxy_type: Union[str, "ProxyType"], + index: int, height: int, ext_index: int, period: Optional[int] = None, @@ -299,8 +318,9 @@ def kill_pure_proxy_extrinsic( subtensor: Subtensor instance with the connection to the chain. wallet: Bittensor wallet object. The wallet.coldkey.ss58_address must be the spawner of the pure proxy (the account that created it via create_pure_proxy_extrinsic). - proxy_ss58: The SS58 address of the pure proxy account to kill. + spawner: The SS58 address of the pure proxy account to kill. proxy_type: The type of proxy permissions. Can be a string or ProxyType enum value. + index: The disambiguation index originally passed to `create_pure`. height: The block height at which the pure proxy was created. ext_index: The extrinsic index at which the pure proxy was created. period: The number of blocks during which the transaction will remain valid after it's submitted. If the @@ -321,16 +341,10 @@ def kill_pure_proxy_extrinsic( proxy_type_str = ProxyType.normalize(proxy_type) - logging.debug( - f"Killing pure proxy: spawner=[blue]{wallet.coldkey.ss58_address}[/blue], " - f"proxy=[blue]{proxy_ss58}[/blue], type=[blue]{proxy_type_str}[/blue] " - f"on [blue]{subtensor.network}[/blue]." - ) - call = Proxy(subtensor).kill_pure( - spawner=wallet.coldkey.ss58_address, - proxy=proxy_ss58, + spawner=spawner, proxy_type=proxy_type_str, + index=index, height=height, ext_index=ext_index, ) From ffd6443d74392dfc9de80e0da599449bbb157b7e Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Sat, 8 Nov 2025 06:16:27 -0800 Subject: [PATCH 21/62] types - linter --- bittensor/core/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/core/types.py b/bittensor/core/types.py index 65754eec6b..b535170412 100644 --- a/bittensor/core/types.py +++ b/bittensor/core/types.py @@ -358,7 +358,7 @@ class ExtrinsicResponse: extrinsic_function: Optional[str] = None extrinsic: Optional["GenericExtrinsic"] = None extrinsic_fee: Optional["Balance"] = None - extrinsic_receipt: Optional[Union["AsyncExtrinsicReceipt", "ExtrinsicReceipt"]] = ( + extrinsic_receipt: Optional[Union["ExtrinsicReceipt", "AsyncExtrinsicReceipt"]] = ( None ) transaction_tao_fee: Optional["Balance"] = None From 713fc3b350e10674fcdeb6d46ec2ad51843732ce Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Sun, 9 Nov 2025 15:36:34 -0800 Subject: [PATCH 22/62] add consistency tests and related workflow --- .../subtensor-consistency-tests.yaml | 223 ++++++++++++++++++ "tests/\321\201onsistency/__init__.py" | 0 "tests/\321\201onsistency/conftest.py" | 3 + .../\321\201onsistency/test_proxy_types.py" | 27 +++ 4 files changed, 253 insertions(+) create mode 100644 .github/workflows/subtensor-consistency-tests.yaml create mode 100644 "tests/\321\201onsistency/__init__.py" create mode 100644 "tests/\321\201onsistency/conftest.py" create mode 100644 "tests/\321\201onsistency/test_proxy_types.py" diff --git a/.github/workflows/subtensor-consistency-tests.yaml b/.github/workflows/subtensor-consistency-tests.yaml new file mode 100644 index 0000000000..ebf7256422 --- /dev/null +++ b/.github/workflows/subtensor-consistency-tests.yaml @@ -0,0 +1,223 @@ +name: Subtensor Consistency Tests + +concurrency: + group: consistency-subtensor-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +on: + pull_request: + branches: + - '**' + types: [ opened, synchronize, reopened, ready_for_review ] + + workflow_dispatch: + inputs: + verbose: + description: "Output more information when triggered manually" + required: false + default: "" + +# job to run tests in parallel +jobs: + # Looking for e2e tests + find-tests: + runs-on: ubuntu-latest + if: ${{ github.event.pull_request.draft == false }} + outputs: + test-files: ${{ steps.get-tests.outputs.test-files }} + steps: + - name: Check-out repository under $GITHUB_WORKSPACE + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: false + cache-dependency-glob: '**/pyproject.toml' + ignore-nothing-to-cache: true + + - name: Cache uv and venv + uses: actions/cache@v4 + with: + path: | + ~/.cache/uv + .venv + key: uv-${{ runner.os }}-py3.10-${{ hashFiles('pyproject.toml') }} + restore-keys: uv-${{ runner.os }}-py3.10- + + - name: Install dependencies (faster if cache hit) + run: uv sync --extra dev --dev + + - name: Find test files + id: get-tests + shell: bash + run: | + set -euo pipefail + test_matrix=$( + uv run pytest -q --collect-only tests/consistency \ + | sed -n '/^consistency\//p' \ + | sed 's|^|tests/|' \ + | jq -R -s -c ' + split("\n") + | map(select(. != "")) + | map({nodeid: ., label: (sub("^tests/consistency/"; ""))}) + ' + ) + echo "Found tests: $test_matrix" + echo "test-files=$test_matrix" >> "$GITHUB_OUTPUT" + + # Pull docker image + pull-docker-image: + runs-on: ubuntu-latest + outputs: + image-name: ${{ steps.set-image.outputs.image }} + steps: + - name: Set Docker image tag based on label or branch + id: set-image + run: | + echo "Event: $GITHUB_EVENT_NAME" + echo "Branch: $GITHUB_REF_NAME" + + echo "Reading labels ..." + if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then + labels=$(jq -r '.pull_request.labels[].name' "$GITHUB_EVENT_PATH") + else + labels="" + fi + + image="" + + for label in $labels; do + echo "Found label: $label" + case "$label" in + "subtensor-localnet:main") + image="ghcr.io/opentensor/subtensor-localnet:main" + break + ;; + "subtensor-localnet:testnet") + image="ghcr.io/opentensor/subtensor-localnet:testnet" + break + ;; + "subtensor-localnet:devnet") + image="ghcr.io/opentensor/subtensor-localnet:devnet" + break + ;; + esac + done + + if [[ -z "$image" ]]; then + # fallback to default based on branch + if [[ "${GITHUB_REF_NAME}" == "master" ]]; then + image="ghcr.io/opentensor/subtensor-localnet:main" + else + image="ghcr.io/opentensor/subtensor-localnet:devnet-ready" + fi + fi + + echo "✅ Final selected image: $image" + echo "image=$image" >> "$GITHUB_OUTPUT" + + - name: Log in to GitHub Container Registry + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin + + - name: Pull Docker Image + run: docker pull ${{ steps.set-image.outputs.image }} + + - name: Save Docker Image to Cache + run: docker save -o subtensor-localnet.tar ${{ steps.set-image.outputs.image }} + + - name: Upload Docker Image as Artifact + uses: actions/upload-artifact@v4 + with: + name: subtensor-localnet + path: subtensor-localnet.tar + compression-level: 0 + + # Job to run tests in parallel + # Since GH Actions matrix has a limit of 256 jobs, we need to split the tests into multiple jobs with different + # Python versions. To reduce DRY we use reusable workflow. + + consistency-tests: + name: "Consistency test: ${{ matrix.test-file }}" + needs: + - find-tests + - pull-docker-image + runs-on: ubuntu-latest + timeout-minutes: 25 + outputs: + failed: ${{ steps.test-failed.outputs.failed }} + + strategy: + fail-fast: false # Allow other matrix jobs to run even if this job fails + max-parallel: 32 # Set the maximum number of parallel jobs (same as we have cores in ubuntu-latest runner) + matrix: + os: + - ubuntu-latest + test-file: ${{ fromJson(needs.find-tests.outputs.test-files) }} + steps: + - name: Check-out repository + uses: actions/checkout@v4 + with: + ref: master + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Cache uv and venv + uses: actions/cache@v4 + with: + path: | + ~/.cache/uv + .venv + key: uv-${{ runner.os }}-py3.10-${{ hashFiles('pyproject.toml') }} + restore-keys: uv-${{ runner.os }}-py3.10- + + - name: install dependencies + run: uv sync --extra dev --dev + + - name: Download Cached Docker Image + uses: actions/download-artifact@v4 + with: + name: subtensor-localnet + + - name: Load Docker Image + run: docker load -i subtensor-localnet.tar + + - name: Run tests with retry + id: test-failed + env: + FAST_BLOCKS: "1" + LOCALNET_IMAGE_NAME: ${{ needs.pull-docker-image.outputs.image-name }} + run: | + set +e + for i in 1 2 3; do + echo "::group::🔁 Test attempt $i" + uv run pytest ${{ matrix.test-file }} -s + status=$? + if [ $status -eq 0 ]; then + echo "✅ Tests passed on attempt $i" + echo "::endgroup::" + echo "failed=false" >> "$GITHUB_OUTPUT" + break + else + echo "❌ Tests failed on attempt $i" + echo "::endgroup::" + if [ $i -eq 3 ]; then + echo "Tests failed after 3 attempts" + echo "failed=true" >> "$GITHUB_OUTPUT" + exit 1 + fi + echo "Retrying..." + sleep 5 + fi + done diff --git "a/tests/\321\201onsistency/__init__.py" "b/tests/\321\201onsistency/__init__.py" new file mode 100644 index 0000000000..e69de29bb2 diff --git "a/tests/\321\201onsistency/conftest.py" "b/tests/\321\201onsistency/conftest.py" new file mode 100644 index 0000000000..f6029fedc1 --- /dev/null +++ "b/tests/\321\201onsistency/conftest.py" @@ -0,0 +1,3 @@ +from ..e2e_tests.conftest import * + +local_chain = local_chain diff --git "a/tests/\321\201onsistency/test_proxy_types.py" "b/tests/\321\201onsistency/test_proxy_types.py" new file mode 100644 index 0000000000..ab9462f8bc --- /dev/null +++ "b/tests/\321\201onsistency/test_proxy_types.py" @@ -0,0 +1,27 @@ +from bittensor.core.chain_data.proxy import ProxyType +from bittensor.core.extrinsics.pallets import SubtensorModule, Proxy, Balances + + +def get_proxy_type_fields(meta): + """Returns list of fields for ProxyType enum from substrate metadata.""" + type_name = "ProxyType" + fields = [] + for item in meta.portable_registry["types"].value: + type_ = item.get("type") + name = None + if len(type_.get("path")) > 1: + name = type_.get("path")[1] + + if name == type_name: + variants = type_.get("def").get("variant").get("variants") + fields = [v.get("name") for v in variants] + return fields + + +def test_make_sure_proxy_type_has_all_fields(subtensor, alice_wallet): + """Tests that SDK ProxyType have all fields defined in the ProxyType enum.""" + + chain_proxy_type_fields = get_proxy_type_fields(subtensor.substrate.metadata) + + assert len(chain_proxy_type_fields) == len(ProxyType) + assert set(chain_proxy_type_fields) == set(ProxyType.all_types()) From d6381f2243743c7dc8427408abf49d8a11066c72 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Sun, 9 Nov 2025 15:36:42 -0800 Subject: [PATCH 23/62] add e2e tests --- tests/e2e_tests/test_proxy.py | 662 ++++++++++++++++++++++++++++++++++ 1 file changed, 662 insertions(+) create mode 100644 tests/e2e_tests/test_proxy.py diff --git a/tests/e2e_tests/test_proxy.py b/tests/e2e_tests/test_proxy.py new file mode 100644 index 0000000000..8440ba8851 --- /dev/null +++ b/tests/e2e_tests/test_proxy.py @@ -0,0 +1,662 @@ +from bittensor.core.chain_data.proxy import ProxyType +from bittensor.core.extrinsics.pallets import SubtensorModule, Proxy, Balances + + +def test_proxy_and_errors(subtensor, alice_wallet, bob_wallet, charlie_wallet): + """Tests proxy logic. + + Steps: + - Verify that chain has no proxies initially. + - Add proxy with ProxyType.Registration and verify success. + - Attempt to add duplicate proxy and verify error handling. + - Add proxy with ProxyType.Transfer and verify success. + - Verify chain has 2 proxies with correct deposit. + - Verify proxy details match expected values (delegate, type, delay). + - Test get_proxies() returns all proxies in network. + - Test get_proxy_constants() returns valid constants. + - Remove proxy ProxyType.Registration and verify deposit decreases. + - Verify chain has 1 proxy remaining. + - Remove proxy ProxyType.Transfer and verify all proxies removed. + - Attempt to remove non-existent proxy and verify NotFound error. + - Attempt to add proxy with invalid type and verify error. + - Attempt to add self as proxy and verify NoSelfProxy error. + - Test adding proxy with delay = 0 and verify it works. + - Test adding multiple proxy types for same delegate. + - Test adding proxy with different delegate. + """ + real_account_wallet = bob_wallet + delegate_wallet = charlie_wallet + delay = 100 + + # === check that chain has no proxies === + assert not subtensor.proxies.get_proxies_for_real_account( + real_account_ss58=real_account_wallet.coldkey.ss58_address + ) + + # === add proxy with ProxyType.Registration === + response = subtensor.proxies.add_proxy( + wallet=real_account_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + proxy_type=ProxyType.Registration, + delay=delay, + ) + assert response.success, response.message + + # === add the same proxy returns error === + response = subtensor.proxies.add_proxy( + wallet=real_account_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + proxy_type=ProxyType.Registration, + delay=delay, + ) + assert not response.success + assert "Duplicate" in response.message + assert response.error["name"] == "Duplicate" + assert response.error["docs"] == ["Account is already a proxy."] + + # === add proxy with ProxyType.Transfer === + response = subtensor.proxies.add_proxy( + wallet=real_account_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + proxy_type=ProxyType.Transfer, + delay=delay, + ) + assert response.success, response.message + + # === check that chain has 2 proxy === + proxies, deposit = subtensor.proxies.get_proxies_for_real_account( + real_account_ss58=real_account_wallet.coldkey.ss58_address + ) + assert len(proxies) == 2 + assert deposit > 0 + initial_deposit = deposit + + proxy_registration = next( + (p for p in proxies if p.proxy_type == ProxyType.Registration), None + ) + assert proxy_registration is not None + assert proxy_registration.delegate == delegate_wallet.coldkey.ss58_address + assert proxy_registration.proxy_type == ProxyType.Registration + assert proxy_registration.delay == delay + + proxy_transfer = next( + (p for p in proxies if p.proxy_type == ProxyType.Transfer), None + ) + assert proxy_transfer is not None + assert proxy_transfer.delegate == delegate_wallet.coldkey.ss58_address + assert proxy_transfer.proxy_type == ProxyType.Transfer + assert proxy_transfer.delay == delay + + # === Test get_proxies() - all proxies in network === + all_proxies = subtensor.proxies.get_proxies() + assert isinstance(all_proxies, dict) + assert real_account_wallet.coldkey.ss58_address in all_proxies + assert len(all_proxies[real_account_wallet.coldkey.ss58_address]) == 2 + + # === Test get_proxy_constants() === + constants = subtensor.proxies.get_proxy_constants() + assert constants.MaxProxies is not None + assert constants.MaxPending is not None + assert constants.ProxyDepositBase is not None + assert constants.ProxyDepositFactor is not None + + # === remove proxy ProxyType.Registration === + response = subtensor.proxies.remove_proxy( + wallet=real_account_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + proxy_type=ProxyType.Registration, + delay=delay, + ) + assert response.success, response.message + + # === check that chain has 1 proxies === + proxies, deposit = subtensor.proxies.get_proxies_for_real_account( + real_account_ss58=real_account_wallet.coldkey.ss58_address + ) + assert len(proxies) == 1 + assert deposit > 0 + # Deposit should decrease after removing one proxy + assert deposit < initial_deposit + + # === remove proxy ProxyType.Transfer === + response = subtensor.proxies.remove_proxy( + wallet=real_account_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + proxy_type=ProxyType.Transfer, + delay=delay, + ) + assert response.success, response.message + + assert not subtensor.proxies.get_proxies_for_real_account( + real_account_ss58=real_account_wallet.coldkey.ss58_address + ) + + # === remove already deleted or unexisted proxy === + response = subtensor.proxies.remove_proxy( + wallet=real_account_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + proxy_type=ProxyType.Transfer, + delay=delay, + ) + assert not response.success + assert "NotFound" in response.message + assert response.error["name"] == "NotFound" + assert response.error["docs"] == ["Proxy registration not found."] + + # === add proxy with wrong type === + response = subtensor.proxies.add_proxy( + wallet=real_account_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + proxy_type="custom type", + delay=delay, + ) + assert not response.success + assert "Invalid proxy type" in response.message + + # === add proxy to the same account === + response = subtensor.proxies.add_proxy( + wallet=real_account_wallet, + delegate_ss58=real_account_wallet.coldkey.ss58_address, + proxy_type=ProxyType.Registration, + delay=0, + ) + assert not response.success + assert "NoSelfProxy" in response.message + assert response.error["name"] == "NoSelfProxy" + assert response.error["docs"] == ["Cannot add self as proxy."] + + # === Test adding proxy with delay = 0 === + response = subtensor.proxies.add_proxy( + wallet=real_account_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + proxy_type=ProxyType.Staking, + delay=0, + ) + assert response.success, response.message + + # Verify delay = 0 + proxies, _ = subtensor.proxies.get_proxies_for_real_account( + real_account_ss58=real_account_wallet.coldkey.ss58_address + ) + proxy_staking = next( + (p for p in proxies if p.proxy_type == ProxyType.Staking), None + ) + assert proxy_staking is not None + assert proxy_staking.delay == 0 + + # === Test adding multiple proxy types for same delegate === + response = subtensor.proxies.add_proxy( + wallet=real_account_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + proxy_type=ProxyType.ChildKeys, + delay=delay, + ) + assert response.success, response.message + + proxies, _ = subtensor.proxies.get_proxies_for_real_account( + real_account_ss58=real_account_wallet.coldkey.ss58_address + ) + assert len(proxies) == 2 # Staking + ChildKeys + + # === Test adding proxy with different delegate === + response = subtensor.proxies.add_proxy( + wallet=real_account_wallet, + delegate_ss58=alice_wallet.coldkey.ss58_address, + proxy_type=ProxyType.Registration, + delay=delay, + ) + assert response.success, response.message + + proxies, _ = subtensor.proxies.get_proxies_for_real_account( + real_account_ss58=real_account_wallet.coldkey.ss58_address + ) + assert len(proxies) == 3 # Staking + ChildKeys + Registration (alice) + + +def test_create_and_announcement_proxy( + subtensor, alice_wallet, bob_wallet, charlie_wallet +): + """Tests proxy logic with announcement mechanism for delay > 0. + + Steps: + - Add proxy with ProxyType.Any and delay > 0. + - Verify premature proxy call returns Unannounced error. + - Verify premature proxy_announced call without announcement returns error. + - Announce first call (register_network) and verify success. + - Test get_proxy_announcements() returns correct announcements. + - Attempt to execute announced call before delay blocks and verify error. + - Wait for delay blocks to pass. + - Execute proxy_announced call and verify subnet registration success. + - Verify announcement is consumed after execution. + - Verify subnet is not active after registration. + - Announce second call (start_call) for subnet activation. + - Test reject_announcement (real account rejects announcement). + - Verify rejected announcement is removed. + - Test remove_announcement (proxy removes its own announcement). + - Verify removed announcement is no longer present. + - Wait for delay blocks after second announcement. + - Execute proxy_announced call to activate subnet and verify success. + - Test proxy_call with delay = 0 (can be used immediately). + - Test proxy_announced with wrong call_hash and verify error. + """ + # === add proxy again === + real_account_wallet = bob_wallet + delegate_wallet = charlie_wallet + + proxy_type = ProxyType.Any + delay = 30 # cant execute proxy 30 blocks after announcement (not after creation) + + response = subtensor.proxies.add_proxy( + wallet=real_account_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + proxy_type=proxy_type, + delay=delay, + ) + assert response.success, response.message + + # check amount of subnets + assert subtensor.subnets.get_total_subnets() == 2 + + subnet_register_call = SubtensorModule(subtensor).register_network( + hotkey=delegate_wallet.hotkey.ss58_address + ) + subnet_activating_call = SubtensorModule(subtensor).start_call(netuid=2) + + # === premature proxy call === + # if delay > 0 .proxy always returns Unannounced error + response = subtensor.proxies.proxy( + wallet=delegate_wallet, + real_account_ss58=real_account_wallet.coldkey.ss58_address, + force_proxy_type=proxy_type, + call=subnet_activating_call, + ) + assert not response.success + assert "Unannounced" in response.message + assert response.error["name"] == "Unannounced" + assert response.error["docs"] == [ + "Announcement, if made at all, was made too recently." + ] + + # === premature proxy_announced call without announcement === + response = subtensor.proxies.proxy_announced( + wallet=delegate_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + real_account_ss58=real_account_wallet.coldkey.ss58_address, + force_proxy_type=proxy_type, + call=subnet_register_call, + ) + assert not response.success + assert "Unannounced" in response.message + assert response.error["name"] == "Unannounced" + assert response.error["docs"] == [ + "Announcement, if made at all, was made too recently." + ] + + # === Announce first call (register_network) === + call_hash_register = "0x" + subnet_register_call.call_hash.hex() + response = subtensor.proxies.announce_proxy( + wallet=delegate_wallet, + real_account_ss58=real_account_wallet.coldkey.ss58_address, + call_hash=call_hash_register, + ) + assert response.success, response.message + registration_block = subtensor.block + delay + + # === Test get_proxy_announcements() === + announcements = subtensor.proxies.get_proxy_announcements() + assert len(announcements[delegate_wallet.coldkey.ss58_address]) == 1 + + delegate_announcement = announcements[delegate_wallet.coldkey.ss58_address][0] + assert delegate_announcement.call_hash == call_hash_register + assert delegate_announcement.real == real_account_wallet.coldkey.ss58_address + + # === announced call before delay blocks - register subnet === + response = subtensor.proxies.proxy_announced( + wallet=delegate_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + real_account_ss58=real_account_wallet.coldkey.ss58_address, + force_proxy_type=proxy_type, + call=subnet_register_call, + ) + assert not response.success + assert "Unannounced" in response.message + assert response.error["name"] == "Unannounced" + assert response.error["docs"] == [ + "Announcement, if made at all, was made too recently." + ] + + # === delay block need to be awaited after announcement === + subtensor.wait_for_block(registration_block) + + # === proxy call - register subnet === + response = subtensor.proxies.proxy_announced( + wallet=delegate_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + real_account_ss58=real_account_wallet.coldkey.ss58_address, + force_proxy_type=proxy_type, + call=subnet_register_call, + ) + assert response.success, response.message + assert subtensor.subnets.get_total_subnets() == 3 + + # === Verify announcement is consumed (cannot reuse) === + assert not subtensor.proxies.get_proxy_announcements() + + # === check that subnet is not active === + assert not subtensor.subnets.is_subnet_active(netuid=2) + + # === Announce second call (start_call) === + call_hash_activating = "0x" + subnet_activating_call.call_hash.hex() + response = subtensor.proxies.announce_proxy( + wallet=delegate_wallet, + real_account_ss58=real_account_wallet.coldkey.ss58_address, + call_hash=call_hash_activating, + ) + assert response.success, response.message + + # === Test reject_announcement (real account rejects) === + # Create another announcement to test rejection + test_call = SubtensorModule(subtensor).register_network( + hotkey=alice_wallet.hotkey.ss58_address + ) + test_call_hash = "0x" + test_call.call_hash.hex() + + response = subtensor.proxies.announce_proxy( + wallet=delegate_wallet, + real_account_ss58=real_account_wallet.coldkey.ss58_address, + call_hash=test_call_hash, + ) + assert response.success, response.message + + # Real account rejects the announcement + response = subtensor.proxies.reject_proxy_announcement( + wallet=real_account_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + call_hash=test_call_hash, + ) + assert response.success, response.message + + # Verify announcement was removed + announcements = subtensor.proxies.get_proxy_announcement( + delegate_account_ss58=delegate_wallet.coldkey.ss58_address + ) + # Should only have start_call announcement, test_call should be rejected + assert len(announcements) == 1 + assert announcements[0].call_hash == call_hash_activating + + # === Test remove_announcement (proxy removes its own announcement) === + # Create another announcement + test_call2 = SubtensorModule(subtensor).register_network( + hotkey=alice_wallet.hotkey.ss58_address + ) + test_call_hash2 = "0x" + test_call2.call_hash.hex() + + response = subtensor.proxies.announce_proxy( + wallet=delegate_wallet, + real_account_ss58=real_account_wallet.coldkey.ss58_address, + call_hash=test_call_hash2, + ) + assert response.success, response.message + + # Proxy removes its own announcement + response = subtensor.proxies.remove_proxy_announcement( + wallet=delegate_wallet, + real_account_ss58=real_account_wallet.coldkey.ss58_address, + call_hash=test_call_hash2, + ) + assert response.success, response.message + + # Verify announcement was removed + announcements = subtensor.proxies.get_proxy_announcement( + delegate_account_ss58=delegate_wallet.coldkey.ss58_address + ) + assert len(announcements) == 1 + assert announcements[0].call_hash == call_hash_activating + + # === delay block need to be awaited after announcement === + activation_block = subtensor.block + delay + subtensor.wait_for_block(activation_block) + + # === proxy call - activate subnet === + response = subtensor.proxies.proxy_announced( + wallet=delegate_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + real_account_ss58=real_account_wallet.coldkey.ss58_address, + force_proxy_type=proxy_type, + call=subnet_activating_call, + ) + assert response.success, response.message + assert subtensor.subnets.is_subnet_active(netuid=2) + + # === Test proxy_call with delay = 0 (can be used immediately) === + # Add proxy with delay = 0 + response = subtensor.proxies.add_proxy( + wallet=real_account_wallet, + delegate_ss58=alice_wallet.coldkey.ss58_address, + proxy_type=ProxyType.Any, + delay=0, + ) + assert response.success, response.message + + # With delay = 0, can use proxy_call directly without announcement + test_call3 = SubtensorModule(subtensor).register_network( + hotkey=alice_wallet.hotkey.ss58_address + ) + response = subtensor.proxies.proxy( + wallet=alice_wallet, + real_account_ss58=real_account_wallet.coldkey.ss58_address, + force_proxy_type=ProxyType.Any, + call=test_call3, + ) + assert response.success, response.message + + # === Test proxy_announced with wrong call_hash === + # Create announcement + correct_call = SubtensorModule(subtensor).register_network( + hotkey=alice_wallet.hotkey.ss58_address + ) + correct_call_hash = "0x" + correct_call.call_hash.hex() + + response = subtensor.proxies.announce_proxy( + wallet=alice_wallet, + real_account_ss58=real_account_wallet.coldkey.ss58_address, + call_hash=correct_call_hash, + ) + assert response.success, response.message + + # Wait for delay + subtensor.wait_for_block( + subtensor.block + 1 + ) # delay = 0, so can execute immediately + + # Try to execute with wrong call (different call_hash) + wrong_call = SubtensorModule(subtensor).start_call(netuid=3) + response = subtensor.proxies.proxy_announced( + wallet=alice_wallet, + delegate_ss58=alice_wallet.coldkey.ss58_address, + real_account_ss58=real_account_wallet.coldkey.ss58_address, + force_proxy_type=ProxyType.Any, + call=wrong_call, # Wrong call_hash + ) + # Should fail because call_hash doesn't match + assert not response.success + + +def test_create_and_kill_pure_proxy(subtensor, alice_wallet, bob_wallet): + """Tests create_pure_proxy and kill_pure_proxy extrinsics. + + Steps: + - Create pure proxy with specific index. + - Extract pure proxy address from response.data. + - Extract block height and ext_index from response. + - Fund the pure proxy account so it can execute transfers. + - Execute a transfer through the pure proxy to verify it works. + - Kill pure proxy using proxy_extrinsic (spawner acts as any proxy for pure proxy). + - Verify pure proxy is killed by attempting to use it and getting an error. + """ + spawner_wallet = bob_wallet + proxy_type = ProxyType.Any + delay = 0 + index = 0 + + # === Create pure proxy === + response = subtensor.proxies.create_pure_proxy( + wallet=spawner_wallet, + proxy_type=proxy_type, + delay=delay, + index=index, + ) + assert response.success, response.message + + # === Extract pure proxy data from response.data === + pure_account = response.data.get("pure_account") + spawner = response.data.get("spawner") + proxy_type_from_response = response.data.get("proxy_type") + index_from_response = response.data.get("index") + height = response.data.get("height") + ext_index = response.data.get("ext_index") + + # === Verify spawner matches === + assert spawner == spawner_wallet.coldkey.ss58_address + + # === Verify all required data is present === + assert pure_account, "Pure account should be present." + assert spawner, "Spawner should be present." + assert proxy_type_from_response, "Proxy type should be present." + assert isinstance(index_from_response, int) + assert isinstance(height, int) and height > 0 + assert isinstance(ext_index, int) and ext_index >= 0 + + # === Fund the pure proxy account so it can execute transfers === + from bittensor.utils.balance import Balance + + fund_amount = Balance.from_tao(1.0) # Fund with 1 TAO + response = subtensor.wallets.transfer( + wallet=spawner_wallet, + destination_ss58=pure_account, + amount=fund_amount, + ) + assert response.success, f"Failed to fund pure proxy account: {response.message}." + + # === Test that pure proxy works by executing a transfer through it === + transfer_amount = Balance.from_tao(0.1) # Transfer 0.1 TAO + transfer_call = Balances(subtensor).transfer_keep_alive( + dest=alice_wallet.coldkey.ss58_address, + value=transfer_amount.rao, + ) + + # === Execute transfer through pure proxy - spawner signs, but origin is pure_account === + response = subtensor.proxies.proxy( + wallet=spawner_wallet, # Spawner signs the transaction + real_account_ss58=pure_account, # Pure proxy account is the origin (real) + force_proxy_type=ProxyType.Any, # Spawner acts as Any proxy for pure proxy + call=transfer_call, + ) + assert response.success, ( + f"Pure proxy should be able to execute transfers, got: {response.message}." + ) + + # === Kill pure proxy using proxy_extrinsic === + # - Origin must be pure proxy account (keyless) + # - Spawner signs as any proxy for pure proxy + # - Use proxy_extrinsic where spawner signs, but real_account is pure proxy :D damn + + # === Create kill_pure call === + kill_pure_call = Proxy(subtensor).kill_pure( + spawner=spawner, # The account that created the pure proxy + proxy_type=proxy_type_from_response, + index=index_from_response, + height=height, + ext_index=ext_index, + ) + + # === Execute kill_pure via proxy_extrinsic === + # spawner_wallet signs, but origin is pure_account + response = subtensor.proxies.proxy( + wallet=spawner_wallet, # Spawner signs the transaction + real_account_ss58=pure_account, # Pure proxy account is the origin (real) + force_proxy_type=ProxyType.Any, # Spawner acts as Any proxy for pure proxy + call=kill_pure_call, + ) + assert response.success, response.message + + # === Verify pure proxy is killed by attempting to use it === + # Create a simple transfer call to test that proxy fails + simple_call = Balances(subtensor).transfer_keep_alive( + dest=alice_wallet.coldkey.ss58_address, + value=500, # Small amount, just to test + ) + + # === Attempt to execute call through killed pure proxy - should fail === + response = subtensor.proxies.proxy( + wallet=spawner_wallet, + real_account_ss58=pure_account, # Killed pure proxy account + force_proxy_type=ProxyType.Any, + call=simple_call, + ) + + # === Should fail because pure proxy no longer exists === + assert not response.success, "Call through killed pure proxy should fail." + assert "NotProxy" in response.message + assert response.error["name"] == "NotProxy" + assert response.error["docs"] == [ + "Sender is not a proxy of the account to be proxied." + ] + + +def test_remove_proxies(subtensor, alice_wallet, bob_wallet, charlie_wallet): + """Tests remove_proxies extrinsic. + + Steps: + - Add multiple proxies with different types and delegates + - Verify all proxies exist and deposit is correct + - Call remove_proxies to remove all at once + - Verify all proxies are removed + - Verify deposit is returned (should be 0 or empty) + """ + real_account_wallet = bob_wallet + delegate1 = charlie_wallet + delegate2 = alice_wallet + + # === Add multiple proxies === + response = subtensor.proxies.add_proxy( + wallet=real_account_wallet, + delegate_ss58=delegate1.coldkey.ss58_address, + proxy_type=ProxyType.Registration, + delay=0, + ) + assert response.success + + response = subtensor.proxies.add_proxy( + wallet=real_account_wallet, + delegate_ss58=delegate1.coldkey.ss58_address, + proxy_type=ProxyType.Transfer, + delay=0, + ) + assert response.success + + response = subtensor.proxies.add_proxy( + wallet=real_account_wallet, + delegate_ss58=delegate2.coldkey.ss58_address, + proxy_type=ProxyType.Staking, + delay=0, + ) + assert response.success + + # === Verify all proxies exist === + proxies, deposit = subtensor.proxies.get_proxies_for_real_account( + real_account_ss58=real_account_wallet.coldkey.ss58_address + ) + assert len(proxies) == 3 + assert deposit > 0 + + # === Remove all proxies === + response = subtensor.proxies.remove_proxies( + wallet=real_account_wallet, + ) + assert response.success, response.message + + # === Verify all proxies removed === + assert not subtensor.proxies.get_proxies_for_real_account( + real_account_ss58=real_account_wallet.coldkey.ss58_address + ) From 465046475d58be6a30925e98cee28a404b72ec5f Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Sun, 9 Nov 2025 15:38:11 -0800 Subject: [PATCH 24/62] add deprecated type to be able pass consistency test --- bittensor/core/chain_data/proxy.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bittensor/core/chain_data/proxy.py b/bittensor/core/chain_data/proxy.py index cd68771e38..a09d031e6a 100644 --- a/bittensor/core/chain_data/proxy.py +++ b/bittensor/core/chain_data/proxy.py @@ -107,6 +107,12 @@ class ProxyType(str, Enum): SubnetLeaseBeneficiary = "SubnetLeaseBeneficiary" RootClaim = "RootClaim" + # deprecated proxy types + Triumvirate = "Triumvirate" + Governance = "Governance" + Senate = "Senate" + RootWeights = "RootWeights" + @classmethod def all_types(cls) -> list[str]: """Returns a list of all proxy type values.""" From 22fb7e5f15fc95c3d66981c47afbd808eb5c266c Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Sun, 9 Nov 2025 16:18:34 -0800 Subject: [PATCH 25/62] add e2e test `test_poke_deposit` --- tests/e2e_tests/test_proxy.py | 62 +++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/e2e_tests/test_proxy.py b/tests/e2e_tests/test_proxy.py index 8440ba8851..ab94562daf 100644 --- a/tests/e2e_tests/test_proxy.py +++ b/tests/e2e_tests/test_proxy.py @@ -660,3 +660,65 @@ def test_remove_proxies(subtensor, alice_wallet, bob_wallet, charlie_wallet): assert not subtensor.proxies.get_proxies_for_real_account( real_account_ss58=real_account_wallet.coldkey.ss58_address ) + + +def test_poke_deposit(subtensor, alice_wallet, bob_wallet, charlie_wallet): + """Tests poke_deposit extrinsic. + + Steps: + - Add multiple proxies and announcements + - Verify initial deposit amount + - Call poke_deposit to recalculate deposits + - Verify deposit may change (if requirements changed) + - Verify transaction fee is waived if deposit changed + """ + real_account_wallet = bob_wallet + delegate_wallet = charlie_wallet + + # Add proxies + response = subtensor.proxies.add_proxy( + wallet=real_account_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + proxy_type=ProxyType.Registration, + delay=0, + ) + assert response.success + + response = subtensor.proxies.add_proxy( + wallet=real_account_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + proxy_type=ProxyType.Transfer, + delay=0, + ) + assert response.success + + # Get initial deposit + _, initial_deposit = subtensor.proxies.get_proxies_for_real_account( + real_account_ss58=real_account_wallet.coldkey.ss58_address + ) + + # Create an announcement + test_call = SubtensorModule(subtensor).register_network( + hotkey=alice_wallet.hotkey.ss58_address + ) + call_hash = "0x" + test_call.call_hash.hex() + + response = subtensor.proxies.announce_proxy( + wallet=delegate_wallet, + real_account_ss58=real_account_wallet.coldkey.ss58_address, + call_hash=call_hash, + ) + assert response.success + + # Call poke_deposit + response = subtensor.proxies.poke_deposit( + wallet=real_account_wallet, + ) + assert response.success, response.message + + # Verify deposit is still correct (or adjusted) + _, final_deposit = subtensor.proxies.get_proxies_for_real_account( + real_account_ss58=real_account_wallet.coldkey.ss58_address + ) + # Deposit should match or be adjusted based on current requirements + assert final_deposit >= 0 From 52508bc009b0b4879e828190c7001ec46f5a3693 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 10 Nov 2025 09:47:16 -0800 Subject: [PATCH 26/62] consistency --- bittensor/core/chain_data/proxy.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/bittensor/core/chain_data/proxy.py b/bittensor/core/chain_data/proxy.py index a09d031e6a..61fd3da313 100644 --- a/bittensor/core/chain_data/proxy.py +++ b/bittensor/core/chain_data/proxy.py @@ -212,9 +212,7 @@ def from_query(cls, query: Any) -> tuple[list["ProxyInfo"], Balance]: proxies = query.value[0][0] # balance data is always in that path balance = query.value[1] - return ( - (cls.from_tuple(proxies), Balance.from_rao(balance)) if proxies else tuple() - ) + return cls.from_tuple(proxies), Balance.from_rao(balance) @classmethod def from_query_map_record(cls, record: list) -> tuple[str, list["ProxyInfo"]]: @@ -266,7 +264,7 @@ def from_dict(cls, data: tuple) -> list["ProxyAnnouncementInfo"]: data: Tuple of announcements data. Returns: - Tuple of ProxyAnnouncementInfo objects. + Tuple of ProxyAnnouncementInfo objects or None if no announcements aren't found. """ return [ cls( From 638fdad8b42163fb06cf53b0d012470d4c247644 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 10 Nov 2025 09:47:48 -0800 Subject: [PATCH 27/62] correct description --- bittensor/core/extrinsics/proxy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bittensor/core/extrinsics/proxy.py b/bittensor/core/extrinsics/proxy.py index 8cb0a5262c..a00e1881a5 100644 --- a/bittensor/core/extrinsics/proxy.py +++ b/bittensor/core/extrinsics/proxy.py @@ -318,7 +318,8 @@ def kill_pure_proxy_extrinsic( subtensor: Subtensor instance with the connection to the chain. wallet: Bittensor wallet object. The wallet.coldkey.ss58_address must be the spawner of the pure proxy (the account that created it via create_pure_proxy_extrinsic). - spawner: The SS58 address of the pure proxy account to kill. + spawner: spawner: The SS58 address of the spawner account (the account that originally created the pure proxy + via `create_pure_proxy_extrinsic`). This should match wallet.coldkey.ss58_address. proxy_type: The type of proxy permissions. Can be a string or ProxyType enum value. index: The disambiguation index originally passed to `create_pure`. height: The block height at which the pure proxy was created. From b5a1c27fa672339315fe91bf72a0a9290678dc9e Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 10 Nov 2025 09:48:10 -0800 Subject: [PATCH 28/62] add subtensor methods --- bittensor/core/subtensor.py | 676 +++++++++++++++++++++++++++++++++++- 1 file changed, 662 insertions(+), 14 deletions(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 07ba241d00..c5d31736f6 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -23,8 +23,10 @@ MetagraphInfo, NeuronInfo, NeuronInfoLite, + ProxyAnnouncementInfo, ProxyInfo, ProxyConstants, + ProxyType, SelectiveMetagraphIndex, SimSwapResult, StakeInfo, @@ -69,6 +71,19 @@ swap_stake_extrinsic, move_stake_extrinsic, ) +from bittensor.core.extrinsics.proxy import ( + add_proxy_extrinsic, + announce_extrinsic, + create_pure_proxy_extrinsic, + kill_pure_proxy_extrinsic, + poke_deposit_extrinsic, + proxy_announced_extrinsic, + proxy_extrinsic, + reject_announcement_extrinsic, + remove_announcement_extrinsic, + remove_proxy_extrinsic, + remove_proxies_extrinsic, +) from bittensor.core.extrinsics.registration import ( burned_register_extrinsic, register_extrinsic, @@ -2225,19 +2240,58 @@ def get_parents( return [] - def get_proxies( + def get_proxies(self, block: Optional[int] = None) -> dict[str, list[ProxyInfo]]: + """ + Retrieves all proxy relationships from the chain. + + This method queries the Proxy.Proxies storage map across all accounts and returns a dictionary mapping each real + account (delegator) to its list of proxy relationships. + + Parameters: + block: The blockchain block number for the query. If None, queries the latest block. + + Returns: + Dictionary mapping real account SS58 addresses to lists of ProxyInfo objects. Each ProxyInfo + contains the delegate address, proxy type, and delay for that proxy relationship. + + Note: + This method queries all proxy relationships on the chain, which may be resource-intensive for large + networks. Consider using `get_proxies_for_real_account()` for querying specific accounts. + """ + block_hash = self.determine_block_hash(block) + query_map = self.substrate.query_map( + module="Proxy", + storage_function="Proxies", + block_hash=block_hash, + ) + + proxies = {} + for record in query_map: + real_account, proxy_list = ProxyInfo.from_query_map_record(record) + proxies[real_account] = proxy_list + return proxies + + def get_proxies_for_real_account( self, real_account_ss58: str, block: Optional[int] = None, ) -> tuple[list[ProxyInfo], Balance]: - """Returns a list of proxies for the given account. + """ + Returns proxy/ies associated with the provided real account. + + This method queries the Proxy.Proxies storage for a specific real account and returns all proxy relationships + where this real account is the delegator. It also returns the deposit amount reserved for these proxies. Parameters: - real_account_ss58: SS58 address of the real (delegator) account. + real_account_ss58: SS58 address of the real account (delegator) whose proxies to retrieve. block: The blockchain block number for the query. Returns: - The tuple containing a list of ProxiInfo objects and reserved deposit amount. + Tuple containing: + - List of ProxyInfo objects representing all proxy relationships for the real account. Each ProxyInfo + contains delegate address, proxy type, and delay. + - Balance object representing the reserved deposit amount for these proxies. This deposit is held as + long as the proxy relationships exist and is returned when proxies are removed. """ block_hash = self.determine_block_hash(block) query = self.substrate.query( @@ -2246,11 +2300,72 @@ def get_proxies( params=[real_account_ss58], block_hash=block_hash, ) + return ProxyInfo.from_query(query) - if query is None or query.value is None or not query.value[0][0]: - return [], Balance.from_rao(0) + def get_proxy_announcement( + self, + delegate_account_ss58: str, + block: Optional[int] = None, + ) -> list[ProxyAnnouncementInfo]: + """ + Retrieves proxy announcements for a specific delegate account. - return ProxyInfo.from_query(query) + This method queries the Proxy.Announcements storage for announcements made by the given delegate proxy account. + Announcements allow a proxy to declare its intention to execute a call on behalf of a real account after a delay + period. + + Parameters: + delegate_account_ss58: SS58 address of the delegate proxy account whose announcements to retrieve. + block: The blockchain block number for the query. If None, queries the latest block. + + Returns: + List of ProxyAnnouncementInfo objects. Each object contains the real account address, call hash, and block + height at which the announcement was made. + + Note: + If the delegate has no announcements, returns an empty list. + """ + block_hash = self.determine_block_hash(block) + query = self.substrate.query( + module="Proxy", + storage_function="Announcements", + params=[delegate_account_ss58], + block_hash=block_hash, + ) + return ProxyAnnouncementInfo.from_dict(query.value[0]) + + def get_proxy_announcements( + self, + block: Optional[int] = None, + ) -> dict[str, list[ProxyAnnouncementInfo]]: + """ + Retrieves all proxy announcements from the chain. + + This method queries the Proxy.Announcements storage map across all delegate accounts and returns a dictionary + mapping each delegate to its list of pending announcements. + + Parameters: + block: The blockchain block number for the query. If None, queries the latest block. + + Returns: + Dictionary mapping delegate account SS58 addresses to lists of ProxyAnnouncementInfo objects. + Each ProxyAnnouncementInfo contains the real account address, call hash, and block height. + + Note: + This method queries all announcements on the chain, which may be resource-intensive for large networks. + Consider using `get_proxy_announcement()` for querying specific delegates. + """ + block_hash = self.determine_block_hash(block) + query_map = self.substrate.query_map( + module="Proxy", + storage_function="Announcements", + block_hash=block_hash, + ) + announcements = {} + for record in query_map: + delegate, proxy_list = ProxyAnnouncementInfo.from_query_map_record(record) + announcements[delegate] = proxy_list + return announcements def get_proxy_constants( self, @@ -2261,18 +2376,25 @@ def get_proxy_constants( """ Fetches runtime configuration constants from the `Proxy` pallet. - If a list of constant names is provided, only those constants will be queried. - Otherwise, all known constants defined in `ProxyConstants.field_names()` are fetched. + This method retrieves on-chain configuration constants that define deposit requirements, proxy limits, and + announcement constraints for the Proxy pallet. These constants govern how proxy accounts operate within the + Subtensor network. Parameters: - constants: A list of specific constant names to fetch from the pallet. If omitted, all constants from - `ProxyConstants` are queried. + constants: Optional list of specific constant names to fetch. If omitted, all constants defined in + `ProxyConstants.constants_names()` are queried. Valid constant names include: "AnnouncementDepositBase", + "AnnouncementDepositFactor", "MaxProxies", "MaxPending", "ProxyDepositBase", "ProxyDepositFactor". as_dict: If True, returns the constants as a dictionary instead of a `ProxyConstants` object. - block: The blockchain block number for the query. + block: The blockchain block number for the query. If None, queries the latest block. Returns: - ProxyConstants: - A structured dataclass containing the retrieved values. Missing constants are returned as `None`. + If `as_dict` is False: ProxyConstants object containing all requested constants. + If `as_dict` is True: Dictionary mapping constant names to their values (Balance objects for deposit + constants, integers for limit constants). + + Note: + All Balance amounts are returned in RAO. Constants reflect the current chain configuration at the specified + block. """ result = {} const_names = constants or ProxyConstants.constants_names() @@ -4109,6 +4231,102 @@ def add_stake_multiple( wait_for_finalization=wait_for_finalization, ) + def add_proxy( + self, + wallet: "Wallet", + delegate_ss58: str, + proxy_type: Union[str, "ProxyType"], + delay: int, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> ExtrinsicResponse: + """ + Adds a proxy relationship. + + This method creates a proxy relationship where the delegate can execute calls on behalf of the real account (the + wallet owner) with restrictions defined by the proxy type and a delay period. A deposit is required and held as + long as the proxy relationship exists. + + Parameters: + wallet: Bittensor wallet object. + delegate_ss58: The SS58 address of the delegate proxy account. + proxy_type: The type of proxy permissions (e.g., "Any", "NonTransfer", "Governance", "Staking"). Can be a + string or ProxyType enum value. + delay: The number of blocks before the proxy can be used. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + A deposit is required when adding a proxy. The deposit amount is determined by runtime constants and is + returned when the proxy is removed. Use `get_proxy_constants()` to check current deposit requirements. + """ + return add_proxy_extrinsic( + subtensor=self, + wallet=wallet, + delegate_ss58=delegate_ss58, + proxy_type=proxy_type, + delay=delay, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + def announce_proxy( + self, + wallet: "Wallet", + real_account_ss58: str, + call_hash: str, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> ExtrinsicResponse: + """ + Announces a future call that will be executed through a proxy. + + This method allows a proxy account to declare its intention to execute a specific call on behalf of a real + account after a delay period. The real account can review and either approve (via `proxy_announced()`) or reject + (via `reject_proxy_announcement()`) the announcement. + + Parameters: + wallet: Bittensor wallet object (should be the proxy account wallet). + real_account_ss58: The SS58 address of the real account on whose behalf the call will be made. + call_hash: The hash of the call that will be executed in the future. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + A deposit is required when making an announcement. The deposit is returned when the announcement is + executed, rejected, or removed. The announcement can be executed after the delay period has passed. + """ + return announce_extrinsic( + subtensor=self, + wallet=wallet, + real_account_ss58=real_account_ss58, + call_hash=call_hash, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + def burned_register( self, wallet: "Wallet", @@ -4359,6 +4577,56 @@ def create_crowdloan( wait_for_finalization=wait_for_finalization, ) + def create_pure_proxy( + self, + wallet: "Wallet", + proxy_type: Union[str, "ProxyType"], + delay: int, + index: int, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> ExtrinsicResponse: + """ + Creates a pure proxy account. + + A pure proxy is a keyless account that can only be controlled through proxy relationships. Unlike regular + proxies, pure proxies do not have their own private keys, making them more secure for certain use cases. The + pure proxy address is deterministically generated based on the spawner account, proxy type, delay, and index. + + Parameters: + wallet: Bittensor wallet object. + proxy_type: The type of proxy permissions for the pure proxy. Can be a string or ProxyType enum value. + delay: The number of blocks before the pure proxy can be used. + index: The index to use for generating the pure proxy account address. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + The pure proxy account address can be extracted from the "PureCreated" event in the response. Store the + spawner address, proxy_type, index, height, and ext_index as they are required to kill the pure proxy later + via `kill_pure_proxy()`. + """ + return create_pure_proxy_extrinsic( + subtensor=self, + wallet=wallet, + proxy_type=proxy_type, + delay=delay, + index=index, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + def dissolve_crowdloan( self, wallet: "Wallet", @@ -4438,6 +4706,64 @@ def finalize_crowdloan( wait_for_finalization=wait_for_finalization, ) + def kill_pure_proxy( + self, + wallet: "Wallet", + spawner: str, + proxy_type: Union[str, "ProxyType"], + index: int, + height: int, + ext_index: int, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> ExtrinsicResponse: + """ + Kills (removes) a pure proxy account. + + This method removes a pure proxy account that was previously created via `create_pure_proxy()`. The pure proxy + must be killed by executing the `kill_pure` call through the pure proxy itself, using the spawner as a proxy. + This requires the spawner to have an "Any" proxy relationship with the pure proxy. + + Parameters: + wallet: Bittensor wallet object. The wallet.coldkey.ss58_address must be the spawner of the pure proxy (the + account that created it via create_pure_proxy_extrinsic). + spawner: The SS58 address of the spawner account (the account that originally created the pure proxy via + `create_pure_proxy_extrinsic`). This should match wallet.coldkey.ss58_address. + proxy_type: The type of proxy permissions. Can be a string or ProxyType enum value. + index: The disambiguation index originally passed to `create_pure`. + height: The block height at which the pure proxy was created. + ext_index: The extrinsic index at which the pure proxy was created. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + The `kill_pure` call must be executed through the pure proxy account itself, with the spawner acting as a + proxy. This requires the spawner to have an "Any" proxy relationship with the pure proxy. Use `proxy()` to + execute the `kill_pure` call if needed. + """ + return kill_pure_proxy_extrinsic( + subtensor=self, + wallet=wallet, + spawner=spawner, + proxy_type=proxy_type, + index=index, + height=height, + ext_index=ext_index, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + def modify_liquidity( self, wallet: "Wallet", @@ -4561,6 +4887,147 @@ def move_stake( wait_for_finalization=wait_for_finalization, ) + def poke_deposit( + self, + wallet: "Wallet", + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> ExtrinsicResponse: + """ + Adjusts deposits made for proxies and announcements based on current values. + + This method recalculates and updates the locked deposit amounts for both proxy relationships and announcements + for the signing account. It can be used to potentially lower the locked amount if the deposit requirements have + changed (e.g., due to runtime upgrades or changes in the number of proxies/announcements). + + Parameters: + wallet: Bittensor wallet object (the account whose deposits will be adjusted). + period: The number of blocks during which the transaction will remain valid after it's submitted. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + This method automatically adjusts deposits for both proxy relationships and announcements. No parameters are + needed as it operates on the account's current state. + """ + return poke_deposit_extrinsic( + subtensor=self, + wallet=wallet, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + def proxy( + self, + wallet: "Wallet", + real_account_ss58: str, + force_proxy_type: Optional[Union[str, "ProxyType"]], + call: "GenericCall", + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> ExtrinsicResponse: + """ + Executes a call on behalf of the real account through a proxy. + + This method allows a proxy account (delegate) to execute a call on behalf of the real account (delegator). The + call is subject to the permissions defined by the proxy type and must respect the delay period if one was set + when the proxy was added. + + Parameters: + wallet: Bittensor wallet object (should be the proxy account wallet). + real_account_ss58: The SS58 address of the real account on whose behalf the call is being made. + force_proxy_type: The type of proxy to use for the call. If None, any proxy type can be used. Otherwise, must + match one of the allowed proxy types. Can be a string or ProxyType enum value. + call: The inner call to be executed on behalf of the real account. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + The call must be permitted by the proxy type. For example, a "NonTransfer" proxy cannot execute transfer + calls. The delay period must also have passed since the proxy was added. + """ + return proxy_extrinsic( + subtensor=self, + wallet=wallet, + real_account_ss58=real_account_ss58, + force_proxy_type=force_proxy_type, + call=call, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + def proxy_announced( + self, + wallet: "Wallet", + delegate_ss58: str, + real_account_ss58: str, + force_proxy_type: Optional[Union[str, "ProxyType"]], + call: "GenericCall", + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> ExtrinsicResponse: + """ + Executes an announced call on behalf of the real account through a proxy. + + This method executes a call that was previously announced via `announce_proxy()`. The call must match the + call_hash that was announced, and the delay period must have passed since the announcement was made. The real + account has the opportunity to review and reject the announcement before execution. + + Parameters: + wallet: Bittensor wallet object (should be the proxy account wallet that made the announcement). + delegate_ss58: The SS58 address of the delegate proxy account that made the announcement. + real_account_ss58: The SS58 address of the real account on whose behalf the call will be made. + force_proxy_type: The type of proxy to use for the call. If None, any proxy type can be used. Otherwise, must + match one of the allowed proxy types. Can be a string or ProxyType enum value. + call: The inner call to be executed on behalf of the real account (must match the announced call_hash). + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + The call_hash of the provided call must match the call_hash that was announced. The announcement must not + have been rejected by the real account, and the delay period must have passed. + """ + return proxy_announced_extrinsic( + subtensor=self, + wallet=wallet, + delegate_ss58=delegate_ss58, + real_account_ss58=real_account_ss58, + force_proxy_type=force_proxy_type, + call=call, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + def refund_crowdloan( self, wallet: "Wallet", @@ -4605,6 +5072,51 @@ def refund_crowdloan( wait_for_finalization=wait_for_finalization, ) + def reject_proxy_announcement( + self, + wallet: "Wallet", + delegate_ss58: str, + call_hash: str, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> ExtrinsicResponse: + """ + Rejects an announcement made by a proxy delegate. + + This method allows the real account to reject an announcement made by a proxy delegate, preventing the announced + call from being executed. Once rejected, the announcement cannot be executed and the announcement deposit is + returned to the delegate. + + Parameters: + wallet: Bittensor wallet object (should be the real account wallet). + delegate_ss58: The SS58 address of the delegate proxy account whose announcement is being rejected. + call_hash: The hash of the call that was announced and is now being rejected. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + Once rejected, the announcement cannot be executed. The delegate's announcement deposit is returned. + """ + return reject_announcement_extrinsic( + subtensor=self, + wallet=wallet, + delegate_ss58=delegate_ss58, + call_hash=call_hash, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + def register( self, wallet: "Wallet", @@ -4703,6 +5215,52 @@ def register_subnet( wait_for_finalization=wait_for_finalization, ) + def remove_proxy_announcement( + self, + wallet: "Wallet", + real_account_ss58: str, + call_hash: str, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> ExtrinsicResponse: + """ + Removes an announcement made by a proxy account. + + This method allows the proxy account to remove its own announcement before it is executed or rejected. This + frees up the announcement deposit and prevents the call from being executed. Only the proxy account that made + the announcement can remove it. + + Parameters: + wallet: Bittensor wallet object (should be the proxy account wallet that made the announcement). + real_account_ss58: The SS58 address of the real account on whose behalf the call was announced. + call_hash: The hash of the call that was announced and is now being removed. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + Only the proxy account that made the announcement can remove it. The real account can reject it via + `reject_proxy_announcement()`, but cannot remove it directly. + """ + return remove_announcement_extrinsic( + subtensor=self, + wallet=wallet, + real_account_ss58=real_account_ss58, + call_hash=call_hash, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + def remove_liquidity( self, wallet: "Wallet", @@ -4748,6 +5306,96 @@ def remove_liquidity( wait_for_finalization=wait_for_finalization, ) + def remove_proxies( + self, + wallet: "Wallet", + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> ExtrinsicResponse: + """ + Removes all proxy relationships for the account in a single transaction. + + This method removes all proxy relationships for the signing account in a single call, which is more efficient + than removing them one by one using `remove_proxy()`. The deposit for all proxies will be returned to the + account. + + Parameters: + wallet: Bittensor wallet object. The account whose proxies will be removed (the delegator). All proxy + relationships where wallet.coldkey.ss58_address is the real account will be removed. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + This removes all proxy relationships for the account, regardless of proxy type or delegate. Use + `remove_proxy()` if you need to remove specific proxy relationships selectively. + """ + return remove_proxies_extrinsic( + subtensor=self, + wallet=wallet, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + def remove_proxy( + self, + wallet: "Wallet", + delegate_ss58: str, + proxy_type: Union[str, "ProxyType"], + delay: int, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> ExtrinsicResponse: + """ + Removes a specific proxy relationship. + + This method removes a single proxy relationship between the real account and a delegate. The parameters must + exactly match those used when the proxy was added via `add_proxy()`. The deposit for this proxy will be returned + to the account. + + Parameters: + wallet: Bittensor wallet object. + delegate_ss58: The SS58 address of the delegate proxy account to remove. + proxy_type: The type of proxy permissions to remove. Can be a string or ProxyType enum value. + delay: The number of blocks before the proxy removal takes effect. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + The delegate_ss58, proxy_type, and delay parameters must exactly match those used when the proxy was added. + Use `get_proxies_for_real_account()` to retrieve the exact parameters for existing proxies. + """ + return remove_proxy_extrinsic( + subtensor=self, + wallet=wallet, + delegate_ss58=delegate_ss58, + proxy_type=proxy_type, + delay=delay, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + def reveal_weights( self, wallet: "Wallet", From e18171b40ef49f83a7ffb14e2dd94e4f0a369870 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 10 Nov 2025 10:10:32 -0800 Subject: [PATCH 29/62] `kill_pure_proxy` has direct call ability now --- bittensor/core/extrinsics/proxy.py | 94 +++++++++++++++++++++++++----- bittensor/core/subtensor.py | 24 +++++--- tests/e2e_tests/test_proxy.py | 64 +++++++++++--------- 3 files changed, 132 insertions(+), 50 deletions(-) diff --git a/bittensor/core/extrinsics/proxy.py b/bittensor/core/extrinsics/proxy.py index a00e1881a5..3d7db6925c 100644 --- a/bittensor/core/extrinsics/proxy.py +++ b/bittensor/core/extrinsics/proxy.py @@ -301,6 +301,7 @@ def create_pure_proxy_extrinsic( def kill_pure_proxy_extrinsic( subtensor: "Subtensor", wallet: "Wallet", + pure_proxy_ss58: str, spawner: str, proxy_type: Union[str, "ProxyType"], index: int, @@ -314,25 +315,65 @@ def kill_pure_proxy_extrinsic( """ Kills (removes) a pure proxy account. + This method removes a pure proxy account that was previously created via `create_pure_proxy()`. + The `kill_pure` call must be executed through the pure proxy account itself, with the spawner + acting as an "Any" proxy. This method automatically handles this by executing the call via + `proxy()`. + Parameters: subtensor: Subtensor instance with the connection to the chain. - wallet: Bittensor wallet object. The wallet.coldkey.ss58_address must be the spawner of the pure proxy (the - account that created it via create_pure_proxy_extrinsic). - spawner: spawner: The SS58 address of the spawner account (the account that originally created the pure proxy - via `create_pure_proxy_extrinsic`). This should match wallet.coldkey.ss58_address. - proxy_type: The type of proxy permissions. Can be a string or ProxyType enum value. - index: The disambiguation index originally passed to `create_pure`. + wallet: Bittensor wallet object. The wallet.coldkey.ss58_address must be the spawner of + the pure proxy (the account that created it via `create_pure_proxy()`). The spawner + must have an "Any" proxy relationship with the pure proxy. + pure_proxy_ss58: The SS58 address of the pure proxy account to be killed. This is the + address that was returned in the `create_pure_proxy()` response. + spawner: The SS58 address of the spawner account (the account that originally created + the pure proxy via `create_pure_proxy()`). This should match wallet.coldkey.ss58_address. + proxy_type: The type of proxy permissions. Can be a string or ProxyType enum value. Must + match the proxy_type used when creating the pure proxy. + index: The disambiguation index originally passed to `create_pure()`. height: The block height at which the pure proxy was created. ext_index: The extrinsic index at which the pure proxy was created. - 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. + period: The number of blocks during which the transaction will remain valid after it's + submitted. If the transaction is not included in a block within that number of blocks, + it will expire and be rejected. You can think of it as an expiration date for the + transaction. raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. wait_for_inclusion: Whether to wait for the inclusion of the transaction. wait_for_finalization: Whether to wait for the finalization of the transaction. Returns: ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + The `kill_pure` call must be executed through the pure proxy account itself, with the + spawner acting as an "Any" proxy. This method automatically handles this by executing + the call via `proxy()`. The spawner must have an "Any" proxy relationship with the pure + proxy for this to work. + + Example: + # After creating a pure proxy + create_response = subtensor.proxies.create_pure_proxy( + wallet=spawner_wallet, + proxy_type=ProxyType.Any, + delay=0, + index=0, + ) + pure_proxy_ss58 = create_response.data["pure_account"] + spawner = create_response.data["spawner"] + height = create_response.data["height"] + ext_index = create_response.data["ext_index"] + + # Kill the pure proxy + kill_response = subtensor.proxies.kill_pure_proxy( + wallet=spawner_wallet, + pure_proxy_ss58=pure_proxy_ss58, + spawner=spawner, + proxy_type=ProxyType.Any, + index=0, + height=height, + ext_index=ext_index, + ) """ try: if not ( @@ -342,7 +383,25 @@ def kill_pure_proxy_extrinsic( proxy_type_str = ProxyType.normalize(proxy_type) - call = Proxy(subtensor).kill_pure( + # Validate that spawner matches wallet + if wallet.coldkey.ss58_address != spawner: + error_msg = ( + f"Spawner address {spawner} does not match wallet address " + f"{wallet.coldkey.ss58_address}" + ) + if raise_error: + raise ValueError(error_msg) + return ExtrinsicResponse(False, error_msg) + + logging.debug( + f"Killing pure proxy: pure=[blue]{pure_proxy_ss58}[/blue], " + f"spawner=[blue]{spawner}[/blue], type=[blue]{proxy_type_str}[/blue], " + f"index=[blue]{index}[/blue], height=[blue]{height}[/blue], " + f"ext_index=[blue]{ext_index}[/blue] on [blue]{subtensor.network}[/blue]." + ) + + # Create the kill_pure call + kill_pure_call = Proxy(subtensor).kill_pure( spawner=spawner, proxy_type=proxy_type_str, index=index, @@ -350,13 +409,20 @@ def kill_pure_proxy_extrinsic( ext_index=ext_index, ) - response = subtensor.sign_and_send_extrinsic( - call=call, + # Execute kill_pure through proxy() where: + # - wallet (spawner) signs the transaction + # - real_account_ss58 (pure_proxy_ss58) is the origin (pure proxy account) + # - force_proxy_type=Any (spawner acts as Any proxy for pure proxy) + response = proxy_extrinsic( + subtensor=subtensor, wallet=wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, + real_account_ss58=pure_proxy_ss58, + force_proxy_type=ProxyType.Any, + call=kill_pure_call, period=period, raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, ) if response.success: diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index c5d31736f6..a54202456b 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -4709,6 +4709,7 @@ def finalize_crowdloan( def kill_pure_proxy( self, wallet: "Wallet", + pure_proxy_ss58: str, spawner: str, proxy_type: Union[str, "ProxyType"], index: int, @@ -4722,16 +4723,20 @@ def kill_pure_proxy( """ Kills (removes) a pure proxy account. - This method removes a pure proxy account that was previously created via `create_pure_proxy()`. The pure proxy - must be killed by executing the `kill_pure` call through the pure proxy itself, using the spawner as a proxy. - This requires the spawner to have an "Any" proxy relationship with the pure proxy. + This method removes a pure proxy account that was previously created via `create_pure_proxy()`. The `kill_pure` + call must be executed through the pure proxy account itself, with the spawner acting as an "Any" proxy. This + method automatically handles this by executing the call via `proxy()`. Parameters: wallet: Bittensor wallet object. The wallet.coldkey.ss58_address must be the spawner of the pure proxy (the - account that created it via create_pure_proxy_extrinsic). + account that created it via `create_pure_proxy()`). The spawner must have an "Any" proxy relationship + with the pure proxy. + pure_proxy_ss58: The SS58 address of the pure proxy account to be killed. This is the address that was + returned in the `create_pure_proxy()` response. spawner: The SS58 address of the spawner account (the account that originally created the pure proxy via - `create_pure_proxy_extrinsic`). This should match wallet.coldkey.ss58_address. - proxy_type: The type of proxy permissions. Can be a string or ProxyType enum value. + `create_pure_proxy()`). This should match wallet.coldkey.ss58_address. + proxy_type: The type of proxy permissions. Can be a string or ProxyType enum value. Must match the + proxy_type used when creating the pure proxy. index: The disambiguation index originally passed to `create_pure`. height: The block height at which the pure proxy was created. ext_index: The extrinsic index at which the pure proxy was created. @@ -4746,13 +4751,14 @@ def kill_pure_proxy( ExtrinsicResponse: The result object of the extrinsic execution. Note: - The `kill_pure` call must be executed through the pure proxy account itself, with the spawner acting as a - proxy. This requires the spawner to have an "Any" proxy relationship with the pure proxy. Use `proxy()` to - execute the `kill_pure` call if needed. + The `kill_pure` call must be executed through the pure proxy account itself, with the spawner acting as an + "Any" proxy. This method automatically handles this by executing the call via `proxy()`. The spawner must + have an "Any" proxy relationship with the pure proxy for this to work. """ return kill_pure_proxy_extrinsic( subtensor=self, wallet=wallet, + pure_proxy_ss58=pure_proxy_ss58, # НОВЫЙ ПАРАМЕТР spawner=spawner, proxy_type=proxy_type, index=index, diff --git a/tests/e2e_tests/test_proxy.py b/tests/e2e_tests/test_proxy.py index ab94562daf..70320fd576 100644 --- a/tests/e2e_tests/test_proxy.py +++ b/tests/e2e_tests/test_proxy.py @@ -29,9 +29,11 @@ def test_proxy_and_errors(subtensor, alice_wallet, bob_wallet, charlie_wallet): delay = 100 # === check that chain has no proxies === - assert not subtensor.proxies.get_proxies_for_real_account( + proxies, deposit = subtensor.proxies.get_proxies_for_real_account( real_account_ss58=real_account_wallet.coldkey.ss58_address ) + assert not proxies + assert deposit == 0 # === add proxy with ProxyType.Registration === response = subtensor.proxies.add_proxy( @@ -127,9 +129,11 @@ def test_proxy_and_errors(subtensor, alice_wallet, bob_wallet, charlie_wallet): ) assert response.success, response.message - assert not subtensor.proxies.get_proxies_for_real_account( + proxies, deposit = subtensor.proxies.get_proxies_for_real_account( real_account_ss58=real_account_wallet.coldkey.ss58_address ) + assert not proxies + assert deposit == 0 # === remove already deleted or unexisted proxy === response = subtensor.proxies.remove_proxy( @@ -485,14 +489,24 @@ def test_create_and_announcement_proxy( def test_create_and_kill_pure_proxy(subtensor, alice_wallet, bob_wallet): """Tests create_pure_proxy and kill_pure_proxy extrinsics. + This test verifies the complete lifecycle of a pure proxy account: + - Creation of a pure proxy with specific parameters + - Verification that the pure proxy can execute calls through the spawner + - Proper termination of the pure proxy + - Confirmation that the killed pure proxy can no longer be used + Steps: - - Create pure proxy with specific index. - - Extract pure proxy address from response.data. - - Extract block height and ext_index from response. + - Create pure proxy with ProxyType.Any, delay=0, and index=0. + - Extract pure proxy address, spawner, and creation metadata from response.data. + - Verify all required data is present and correctly formatted. - Fund the pure proxy account so it can execute transfers. - - Execute a transfer through the pure proxy to verify it works. - - Kill pure proxy using proxy_extrinsic (spawner acts as any proxy for pure proxy). - - Verify pure proxy is killed by attempting to use it and getting an error. + - Execute a transfer through the pure proxy to verify it works correctly. + The spawner acts as an "Any" proxy for the pure proxy account. + - Kill the pure proxy using kill_pure_proxy() method, which automatically + executes the kill_pure call through proxy() (spawner acts as Any proxy + for pure proxy, with pure proxy as the origin). + - Verify pure proxy is killed by attempting to use it and confirming + it returns a NotProxy error. """ spawner_wallet = bob_wallet proxy_type = ProxyType.Any @@ -539,13 +553,14 @@ def test_create_and_kill_pure_proxy(subtensor, alice_wallet, bob_wallet): assert response.success, f"Failed to fund pure proxy account: {response.message}." # === Test that pure proxy works by executing a transfer through it === + # The spawner acts as an "Any" proxy for the pure proxy account. + # The pure proxy account is the origin (real account), and the spawner signs the transaction. transfer_amount = Balance.from_tao(0.1) # Transfer 0.1 TAO transfer_call = Balances(subtensor).transfer_keep_alive( dest=alice_wallet.coldkey.ss58_address, value=transfer_amount.rao, ) - # === Execute transfer through pure proxy - spawner signs, but origin is pure_account === response = subtensor.proxies.proxy( wallet=spawner_wallet, # Spawner signs the transaction real_account_ss58=pure_account, # Pure proxy account is the origin (real) @@ -556,28 +571,21 @@ def test_create_and_kill_pure_proxy(subtensor, alice_wallet, bob_wallet): f"Pure proxy should be able to execute transfers, got: {response.message}." ) - # === Kill pure proxy using proxy_extrinsic === - # - Origin must be pure proxy account (keyless) - # - Spawner signs as any proxy for pure proxy - # - Use proxy_extrinsic where spawner signs, but real_account is pure proxy :D damn - - # === Create kill_pure call === - kill_pure_call = Proxy(subtensor).kill_pure( - spawner=spawner, # The account that created the pure proxy + # === Kill pure proxy using kill_pure_proxy() method === + # The kill_pure_proxy() method automatically executes the kill_pure call through proxy(): + # - The spawner signs the transaction (wallet parameter) + # - The pure proxy account is the origin (real_account_ss58 parameter) + # - The spawner acts as an "Any" proxy for the pure proxy (force_proxy_type=Any) + # This is required because pure proxies are keyless accounts and cannot sign transactions directly. + response = subtensor.proxies.kill_pure_proxy( + wallet=spawner_wallet, + pure_proxy_ss58=pure_account, + spawner=spawner, proxy_type=proxy_type_from_response, index=index_from_response, height=height, ext_index=ext_index, ) - - # === Execute kill_pure via proxy_extrinsic === - # spawner_wallet signs, but origin is pure_account - response = subtensor.proxies.proxy( - wallet=spawner_wallet, # Spawner signs the transaction - real_account_ss58=pure_account, # Pure proxy account is the origin (real) - force_proxy_type=ProxyType.Any, # Spawner acts as Any proxy for pure proxy - call=kill_pure_call, - ) assert response.success, response.message # === Verify pure proxy is killed by attempting to use it === @@ -657,9 +665,11 @@ def test_remove_proxies(subtensor, alice_wallet, bob_wallet, charlie_wallet): assert response.success, response.message # === Verify all proxies removed === - assert not subtensor.proxies.get_proxies_for_real_account( + proxies, deposit = subtensor.proxies.get_proxies_for_real_account( real_account_ss58=real_account_wallet.coldkey.ss58_address ) + assert not proxies + assert deposit == 0 def test_poke_deposit(subtensor, alice_wallet, bob_wallet, charlie_wallet): From b2e273104b92277353d304564b860086640b57e1 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 10 Nov 2025 10:25:31 -0800 Subject: [PATCH 30/62] wrong return in docstring --- bittensor/core/chain_data/proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/core/chain_data/proxy.py b/bittensor/core/chain_data/proxy.py index 61fd3da313..456b92f388 100644 --- a/bittensor/core/chain_data/proxy.py +++ b/bittensor/core/chain_data/proxy.py @@ -184,7 +184,7 @@ def from_tuple(cls, data: tuple) -> list["ProxyInfo"]: data: Tuple of chain proxy data. Returns: - Tuple of ProxyInfo objects. + List of ProxyInfo objects. """ return [ cls( From c17a0da4012cee8d92303cee2002c4c246b788b2 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 10 Nov 2025 10:31:33 -0800 Subject: [PATCH 31/62] unit tests for query methods --- tests/unit_tests/test_subtensor.py | 581 +++++++++++++++++++++++++++++ 1 file changed, 581 insertions(+) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 5dc104ab51..993524f66a 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -5032,3 +5032,584 @@ def test_get_ema_tao_inflow(subtensor, mocker): ) mocked_fixed_to_float.assert_called_once_with(fake_tao_bits) assert result == (fake_block_updated, Balance.from_rao(1000000)) + + +def test_get_proxies_success(subtensor, mocker): + """Test get_proxies returns correct data when proxy information is found.""" + # Prep + block = 123 + fake_real_account1 = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + fake_real_account2 = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" + fake_proxy_data1 = [ + { + "delegate": {"Id": b"\x00" * 32}, + "proxy_type": {"Any": None}, + "delay": 0, + } + ] + fake_proxy_data2 = [ + { + "delegate": {"Id": b"\x01" * 32}, + "proxy_type": {"Transfer": None}, + "delay": 100, + } + ] + fake_query_map_records = [ + (fake_real_account1.encode(), mocker.Mock(value=([fake_proxy_data1], 1000000))), + (fake_real_account2.encode(), mocker.Mock(value=([fake_proxy_data2], 2000000))), + ] + + mocked_determine_block_hash = mocker.patch.object( + subtensor, "determine_block_hash", return_value="mock_block_hash" + ) + mocked_query_map = mocker.patch.object( + subtensor.substrate, + "query_map", + return_value=fake_query_map_records, + ) + mocked_from_query_map_record = mocker.patch.object( + subtensor_module.ProxyInfo, + "from_query_map_record", + side_effect=[ + (fake_real_account1, [mocker.Mock()]), + (fake_real_account2, [mocker.Mock()]), + ], + ) + + # Call + result = subtensor.get_proxies(block=block) + + # Asserts + mocked_determine_block_hash.assert_called_once_with(block) + mocked_query_map.assert_called_once_with( + module="Proxy", + storage_function="Proxies", + block_hash="mock_block_hash", + ) + assert mocked_from_query_map_record.call_count == 2 + assert isinstance(result, dict) + assert fake_real_account1 in result + assert fake_real_account2 in result + + +def test_get_proxies_no_data(subtensor, mocker): + """Test get_proxies returns empty dict when no proxy information is found.""" + # Prep + block = 123 + mocked_determine_block_hash = mocker.patch.object( + subtensor, "determine_block_hash", return_value="mock_block_hash" + ) + mocked_query_map = mocker.patch.object( + subtensor.substrate, + "query_map", + return_value=[], + ) + + # Call + result = subtensor.get_proxies(block=block) + + # Asserts + mocked_determine_block_hash.assert_called_once_with(block) + mocked_query_map.assert_called_once_with( + module="Proxy", + storage_function="Proxies", + block_hash="mock_block_hash", + ) + assert result == {} + + +def test_get_proxies_no_block(subtensor, mocker): + """Test get_proxies with no block specified.""" + # Prep + mocked_determine_block_hash = mocker.patch.object( + subtensor, "determine_block_hash", return_value="mock_block_hash" + ) + mocked_query_map = mocker.patch.object( + subtensor.substrate, + "query_map", + return_value=[], + ) + + # Call + result = subtensor.get_proxies() + + # Asserts + mocked_determine_block_hash.assert_called_once_with(None) + mocked_query_map.assert_called_once_with( + module="Proxy", + storage_function="Proxies", + block_hash="mock_block_hash", + ) + assert result == {} + + +def test_get_proxies_for_real_account_success(subtensor, mocker): + """Test get_proxies_for_real_account returns correct data when proxy information is found.""" + # Prep + fake_real_account_ss58 = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + block = 123 + fake_proxy_data = [ + { + "delegate": {"Id": b"\x00" * 32}, + "proxy_type": {"Any": None}, + "delay": 0, + } + ] + fake_balance = 1000000 + fake_query_result = mocker.Mock(value=([fake_proxy_data], fake_balance)) + + mocked_determine_block_hash = mocker.patch.object( + subtensor, "determine_block_hash", return_value="mock_block_hash" + ) + mocked_query = mocker.patch.object( + subtensor.substrate, + "query", + return_value=fake_query_result, + ) + mocked_from_query = mocker.patch.object( + subtensor_module.ProxyInfo, + "from_query", + return_value=([mocker.Mock()], Balance.from_rao(fake_balance)), + ) + + # Call + result = subtensor.get_proxies_for_real_account( + real_account_ss58=fake_real_account_ss58, block=block + ) + + # Asserts + mocked_determine_block_hash.assert_called_once_with(block) + mocked_query.assert_called_once_with( + module="Proxy", + storage_function="Proxies", + params=[fake_real_account_ss58], + block_hash="mock_block_hash", + ) + mocked_from_query.assert_called_once_with(fake_query_result) + assert isinstance(result, tuple) + assert len(result) == 2 + assert isinstance(result[0], list) + assert isinstance(result[1], Balance) + + +def test_get_proxies_for_real_account_no_data(subtensor, mocker): + """Test get_proxies_for_real_account returns empty list and zero balance when no proxy information is found.""" + # Prep + fake_real_account_ss58 = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + block = 123 + fake_query_result = mocker.Mock(value=([], 0)) + + mocked_determine_block_hash = mocker.patch.object( + subtensor, "determine_block_hash", return_value="mock_block_hash" + ) + mocked_query = mocker.patch.object( + subtensor.substrate, + "query", + return_value=fake_query_result, + ) + mocked_from_query = mocker.patch.object( + subtensor_module.ProxyInfo, + "from_query", + return_value=([], Balance.from_rao(0)), + ) + + # Call + result = subtensor.get_proxies_for_real_account( + real_account_ss58=fake_real_account_ss58, block=block + ) + + # Asserts + mocked_determine_block_hash.assert_called_once_with(block) + mocked_query.assert_called_once_with( + module="Proxy", + storage_function="Proxies", + params=[fake_real_account_ss58], + block_hash="mock_block_hash", + ) + mocked_from_query.assert_called_once_with(fake_query_result) + assert result == ([], Balance.from_rao(0)) + + +def test_get_proxies_for_real_account_no_block(subtensor, mocker): + """Test get_proxies_for_real_account with no block specified.""" + # Prep + fake_real_account_ss58 = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + fake_query_result = mocker.Mock(value=([], 0)) + + mocked_determine_block_hash = mocker.patch.object( + subtensor, "determine_block_hash", return_value="mock_block_hash" + ) + mocked_query = mocker.patch.object( + subtensor.substrate, + "query", + return_value=fake_query_result, + ) + mocked_from_query = mocker.patch.object( + subtensor_module.ProxyInfo, + "from_query", + return_value=([], Balance.from_rao(0)), + ) + + # Call + result = subtensor.get_proxies_for_real_account( + real_account_ss58=fake_real_account_ss58 + ) + + # Asserts + mocked_determine_block_hash.assert_called_once_with(None) + mocked_query.assert_called_once_with( + module="Proxy", + storage_function="Proxies", + params=[fake_real_account_ss58], + block_hash="mock_block_hash", + ) + mocked_from_query.assert_called_once_with(fake_query_result) + assert result == ([], Balance.from_rao(0)) + + +def test_get_proxy_announcement_success(subtensor, mocker): + """Test get_proxy_announcement returns correct data when announcement information is found.""" + # Prep + fake_delegate_account_ss58 = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + block = 123 + fake_announcement_data = [ + { + "real": {"Id": b"\x00" * 32}, + "call_hash": {"H256": b"\x01" * 32}, + "height": 100, + } + ] + fake_query_result = mocker.Mock(value=[fake_announcement_data]) + + mocked_determine_block_hash = mocker.patch.object( + subtensor, "determine_block_hash", return_value="mock_block_hash" + ) + mocked_query = mocker.patch.object( + subtensor.substrate, + "query", + return_value=fake_query_result, + ) + mocked_from_dict = mocker.patch.object( + subtensor_module.ProxyAnnouncementInfo, + "from_dict", + return_value=[mocker.Mock()], + ) + + # Call + result = subtensor.get_proxy_announcement( + delegate_account_ss58=fake_delegate_account_ss58, block=block + ) + + # Asserts + mocked_determine_block_hash.assert_called_once_with(block) + mocked_query.assert_called_once_with( + module="Proxy", + storage_function="Announcements", + params=[fake_delegate_account_ss58], + block_hash="mock_block_hash", + ) + mocked_from_dict.assert_called_once_with(fake_query_result.value[0]) + assert isinstance(result, list) + + +def test_get_proxy_announcement_no_data(subtensor, mocker): + """Test get_proxy_announcement returns empty list when no announcement information is found.""" + # Prep + fake_delegate_account_ss58 = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + block = 123 + fake_query_result = mocker.Mock(value=[[]]) + + mocked_determine_block_hash = mocker.patch.object( + subtensor, "determine_block_hash", return_value="mock_block_hash" + ) + mocked_query = mocker.patch.object( + subtensor.substrate, + "query", + return_value=fake_query_result, + ) + mocked_from_dict = mocker.patch.object( + subtensor_module.ProxyAnnouncementInfo, + "from_dict", + return_value=[], + ) + + # Call + result = subtensor.get_proxy_announcement( + delegate_account_ss58=fake_delegate_account_ss58, block=block + ) + + # Asserts + mocked_determine_block_hash.assert_called_once_with(block) + mocked_query.assert_called_once_with( + module="Proxy", + storage_function="Announcements", + params=[fake_delegate_account_ss58], + block_hash="mock_block_hash", + ) + mocked_from_dict.assert_called_once_with(fake_query_result.value[0]) + assert result == [] + + +def test_get_proxy_announcement_no_block(subtensor, mocker): + """Test get_proxy_announcement with no block specified.""" + # Prep + fake_delegate_account_ss58 = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + fake_query_result = mocker.Mock(value=[[]]) + + mocked_determine_block_hash = mocker.patch.object( + subtensor, "determine_block_hash", return_value="mock_block_hash" + ) + mocked_query = mocker.patch.object( + subtensor.substrate, + "query", + return_value=fake_query_result, + ) + mocked_from_dict = mocker.patch.object( + subtensor_module.ProxyAnnouncementInfo, + "from_dict", + return_value=[], + ) + + # Call + result = subtensor.get_proxy_announcement( + delegate_account_ss58=fake_delegate_account_ss58 + ) + + # Asserts + mocked_determine_block_hash.assert_called_once_with(None) + mocked_query.assert_called_once_with( + module="Proxy", + storage_function="Announcements", + params=[fake_delegate_account_ss58], + block_hash="mock_block_hash", + ) + mocked_from_dict.assert_called_once_with(fake_query_result.value[0]) + assert result == [] + + +def test_get_proxy_announcements_success(subtensor, mocker): + """Test get_proxy_announcements returns correct data when announcement information is found.""" + # Prep + block = 123 + fake_delegate1 = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + fake_delegate2 = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" + fake_announcement_data1 = [ + { + "real": {"Id": b"\x00" * 32}, + "call_hash": {"H256": b"\x01" * 32}, + "height": 100, + } + ] + fake_announcement_data2 = [ + { + "real": {"Id": b"\x02" * 32}, + "call_hash": {"H256": b"\x03" * 32}, + "height": 200, + } + ] + fake_query_map_records = [ + (fake_delegate1.encode(), mocker.Mock(value=[fake_announcement_data1])), + (fake_delegate2.encode(), mocker.Mock(value=[fake_announcement_data2])), + ] + + mocked_determine_block_hash = mocker.patch.object( + subtensor, "determine_block_hash", return_value="mock_block_hash" + ) + mocked_query_map = mocker.patch.object( + subtensor.substrate, + "query_map", + return_value=fake_query_map_records, + ) + mocked_from_query_map_record = mocker.patch.object( + subtensor_module.ProxyAnnouncementInfo, + "from_query_map_record", + side_effect=[ + (fake_delegate1, [mocker.Mock()]), + (fake_delegate2, [mocker.Mock()]), + ], + ) + + # Call + result = subtensor.get_proxy_announcements(block=block) + + # Asserts + mocked_determine_block_hash.assert_called_once_with(block) + mocked_query_map.assert_called_once_with( + module="Proxy", + storage_function="Announcements", + block_hash="mock_block_hash", + ) + assert mocked_from_query_map_record.call_count == 2 + assert isinstance(result, dict) + assert fake_delegate1 in result + assert fake_delegate2 in result + + +def test_get_proxy_announcements_no_data(subtensor, mocker): + """Test get_proxy_announcements returns empty dict when no announcement information is found.""" + # Prep + block = 123 + mocked_determine_block_hash = mocker.patch.object( + subtensor, "determine_block_hash", return_value="mock_block_hash" + ) + mocked_query_map = mocker.patch.object( + subtensor.substrate, + "query_map", + return_value=[], + ) + + # Call + result = subtensor.get_proxy_announcements(block=block) + + # Asserts + mocked_determine_block_hash.assert_called_once_with(block) + mocked_query_map.assert_called_once_with( + module="Proxy", + storage_function="Announcements", + block_hash="mock_block_hash", + ) + assert result == {} + + +def test_get_proxy_announcements_no_block(subtensor, mocker): + """Test get_proxy_announcements with no block specified.""" + # Prep + mocked_determine_block_hash = mocker.patch.object( + subtensor, "determine_block_hash", return_value="mock_block_hash" + ) + mocked_query_map = mocker.patch.object( + subtensor.substrate, + "query_map", + return_value=[], + ) + + # Call + result = subtensor.get_proxy_announcements() + + # Asserts + mocked_determine_block_hash.assert_called_once_with(None) + mocked_query_map.assert_called_once_with( + module="Proxy", + storage_function="Announcements", + block_hash="mock_block_hash", + ) + assert result == {} + + +def test_get_proxy_constants_success(subtensor, mocker): + """Test get_proxy_constants returns correct data when constants are found.""" + # Prep + block = 123 + fake_constants = { + "AnnouncementDepositBase": 1000000, + "AnnouncementDepositFactor": 500000, + "MaxProxies": 32, + "MaxPending": 32, + "ProxyDepositBase": 2000000, + "ProxyDepositFactor": 1000000, + } + + mocked_query_constant = mocker.patch.object( + subtensor, + "query_constant", + side_effect=[ + mocker.Mock(value=value) for value in fake_constants.values() + ], + ) + mocked_from_dict = mocker.patch.object( + subtensor_module.ProxyConstants, + "from_dict", + return_value=mocker.Mock(), + ) + mocked_to_dict = mocker.patch.object( + mocked_from_dict.return_value, + "to_dict", + return_value=fake_constants, + ) + + # Call + result = subtensor.get_proxy_constants(block=block) + + # Asserts + assert mocked_query_constant.call_count == len(fake_constants) + mocked_from_dict.assert_called_once_with(fake_constants) + assert result == mocked_from_dict.return_value + + +def test_get_proxy_constants_as_dict(subtensor, mocker): + """Test get_proxy_constants returns dict when as_dict=True.""" + # Prep + block = 123 + fake_constants = { + "AnnouncementDepositBase": 1000000, + "AnnouncementDepositFactor": 500000, + "MaxProxies": 32, + "MaxPending": 32, + "ProxyDepositBase": 2000000, + "ProxyDepositFactor": 1000000, + } + + mocked_query_constant = mocker.patch.object( + subtensor, + "query_constant", + side_effect=[ + mocker.Mock(value=value) for value in fake_constants.values() + ], + ) + mocked_proxy_constants = mocker.Mock() + mocked_from_dict = mocker.patch.object( + subtensor_module.ProxyConstants, + "from_dict", + return_value=mocked_proxy_constants, + ) + mocked_to_dict = mocker.patch.object( + mocked_proxy_constants, + "to_dict", + return_value=fake_constants, + ) + + # Call + result = subtensor.get_proxy_constants(block=block, as_dict=True) + + # Asserts + assert mocked_query_constant.call_count == len(fake_constants) + mocked_from_dict.assert_called_once_with(fake_constants) + mocked_to_dict.assert_called_once() + assert result == fake_constants + + +def test_get_proxy_constants_specific_constants(subtensor, mocker): + """Test get_proxy_constants with specific constants list.""" + # Prep + block = 123 + requested_constants = ["MaxProxies", "MaxPending"] + fake_constants = { + "MaxProxies": 32, + "MaxPending": 32, + } + + mocked_query_constant = mocker.patch.object( + subtensor, + "query_constant", + side_effect=[ + mocker.Mock(value=value) for value in fake_constants.values() + ], + ) + mocked_from_dict = mocker.patch.object( + subtensor_module.ProxyConstants, + "from_dict", + return_value=mocker.Mock(), + ) + + # Call + result = subtensor.get_proxy_constants(constants=requested_constants, block=block) + + # Asserts + assert mocked_query_constant.call_count == len(requested_constants) + for const_name in requested_constants: + mocked_query_constant.assert_any_call( + module_name="Proxy", + constant_name=const_name, + block=block, + ) + mocked_from_dict.assert_called_once_with(fake_constants) \ No newline at end of file From db5f720fd858385eb9fb9d4b22224cfdc09d1eb8 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 10 Nov 2025 10:39:10 -0800 Subject: [PATCH 32/62] unit tests for extrinsic methods --- tests/unit_tests/test_subtensor.py | 357 ++++++++++++++++++++++++++++- 1 file changed, 356 insertions(+), 1 deletion(-) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 993524f66a..c57840776b 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -5612,4 +5612,359 @@ def test_get_proxy_constants_specific_constants(subtensor, mocker): constant_name=const_name, block=block, ) - mocked_from_dict.assert_called_once_with(fake_constants) \ No newline at end of file + mocked_from_dict.assert_called_once_with(fake_constants) + + +def test_add_proxy(mocker, subtensor): + """Tests `add_proxy` extrinsic call method.""" + # preps + wallet = mocker.Mock(spec=Wallet) + delegate_ss58 = mocker.Mock(spec=str) + proxy_type = mocker.Mock(spec=str) + delay = mocker.Mock(spec=int) + mocked_add_proxy_extrinsic = mocker.patch.object( + subtensor_module, "add_proxy_extrinsic" + ) + + # call + response = subtensor.add_proxy( + wallet=wallet, + delegate_ss58=delegate_ss58, + proxy_type=proxy_type, + delay=delay, + ) + + # asserts + mocked_add_proxy_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=wallet, + delegate_ss58=delegate_ss58, + proxy_type=proxy_type, + delay=delay, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_add_proxy_extrinsic.return_value + + +def test_announce_proxy(mocker, subtensor): + """Tests `announce_proxy` extrinsic call method.""" + # preps + wallet = mocker.Mock(spec=Wallet) + real_account_ss58 = mocker.Mock(spec=str) + call_hash = mocker.Mock(spec=str) + mocked_announce_extrinsic = mocker.patch.object( + subtensor_module, "announce_extrinsic" + ) + + # call + response = subtensor.announce_proxy( + wallet=wallet, + real_account_ss58=real_account_ss58, + call_hash=call_hash, + ) + + # asserts + mocked_announce_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=wallet, + real_account_ss58=real_account_ss58, + call_hash=call_hash, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_announce_extrinsic.return_value + + +def test_create_pure_proxy(mocker, subtensor): + """Tests `create_pure_proxy` extrinsic call method.""" + # preps + wallet = mocker.Mock(spec=Wallet) + proxy_type = mocker.Mock(spec=str) + delay = mocker.Mock(spec=int) + index = mocker.Mock(spec=int) + mocked_create_pure_proxy_extrinsic = mocker.patch.object( + subtensor_module, "create_pure_proxy_extrinsic" + ) + + # call + response = subtensor.create_pure_proxy( + wallet=wallet, + proxy_type=proxy_type, + delay=delay, + index=index, + ) + + # asserts + mocked_create_pure_proxy_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=wallet, + proxy_type=proxy_type, + delay=delay, + index=index, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_create_pure_proxy_extrinsic.return_value + + +def test_kill_pure_proxy(mocker, subtensor): + """Tests `kill_pure_proxy` extrinsic call method.""" + # preps + wallet = mocker.Mock(spec=Wallet) + pure_proxy_ss58 = mocker.Mock(spec=str) + spawner = mocker.Mock(spec=str) + proxy_type = mocker.Mock(spec=str) + index = mocker.Mock(spec=int) + height = mocker.Mock(spec=int) + ext_index = mocker.Mock(spec=int) + mocked_kill_pure_proxy_extrinsic = mocker.patch.object( + subtensor_module, "kill_pure_proxy_extrinsic" + ) + + # call + response = subtensor.kill_pure_proxy( + wallet=wallet, + pure_proxy_ss58=pure_proxy_ss58, + spawner=spawner, + proxy_type=proxy_type, + index=index, + height=height, + ext_index=ext_index, + ) + + # asserts + mocked_kill_pure_proxy_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=wallet, + pure_proxy_ss58=pure_proxy_ss58, + spawner=spawner, + proxy_type=proxy_type, + index=index, + height=height, + ext_index=ext_index, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_kill_pure_proxy_extrinsic.return_value + + +def test_poke_deposit(mocker, subtensor): + """Tests `poke_deposit` extrinsic call method.""" + # preps + wallet = mocker.Mock(spec=Wallet) + mocked_poke_deposit_extrinsic = mocker.patch.object( + subtensor_module, "poke_deposit_extrinsic" + ) + + # call + response = subtensor.poke_deposit(wallet=wallet) + + # asserts + mocked_poke_deposit_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=wallet, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_poke_deposit_extrinsic.return_value + + +def test_proxy(mocker, subtensor): + """Tests `proxy` extrinsic call method.""" + # preps + wallet = mocker.Mock(spec=Wallet) + real_account_ss58 = mocker.Mock(spec=str) + force_proxy_type = mocker.Mock(spec=str) + call = mocker.Mock(spec=GenericCall) + mocked_proxy_extrinsic = mocker.patch.object( + subtensor_module, "proxy_extrinsic" + ) + + # call + response = subtensor.proxy( + wallet=wallet, + real_account_ss58=real_account_ss58, + force_proxy_type=force_proxy_type, + call=call, + ) + + # asserts + mocked_proxy_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=wallet, + real_account_ss58=real_account_ss58, + force_proxy_type=force_proxy_type, + call=call, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_proxy_extrinsic.return_value + + +def test_proxy_announced(mocker, subtensor): + """Tests `proxy_announced` extrinsic call method.""" + # preps + wallet = mocker.Mock(spec=Wallet) + delegate_ss58 = mocker.Mock(spec=str) + real_account_ss58 = mocker.Mock(spec=str) + force_proxy_type = mocker.Mock(spec=str) + call = mocker.Mock(spec=GenericCall) + mocked_proxy_announced_extrinsic = mocker.patch.object( + subtensor_module, "proxy_announced_extrinsic" + ) + + # call + response = subtensor.proxy_announced( + wallet=wallet, + delegate_ss58=delegate_ss58, + real_account_ss58=real_account_ss58, + force_proxy_type=force_proxy_type, + call=call, + ) + + # asserts + mocked_proxy_announced_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=wallet, + delegate_ss58=delegate_ss58, + real_account_ss58=real_account_ss58, + force_proxy_type=force_proxy_type, + call=call, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_proxy_announced_extrinsic.return_value + + +def test_reject_proxy_announcement(mocker, subtensor): + """Tests `reject_proxy_announcement` extrinsic call method.""" + # preps + wallet = mocker.Mock(spec=Wallet) + delegate_ss58 = mocker.Mock(spec=str) + call_hash = mocker.Mock(spec=str) + mocked_reject_announcement_extrinsic = mocker.patch.object( + subtensor_module, "reject_announcement_extrinsic" + ) + + # call + response = subtensor.reject_proxy_announcement( + wallet=wallet, + delegate_ss58=delegate_ss58, + call_hash=call_hash, + ) + + # asserts + mocked_reject_announcement_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=wallet, + delegate_ss58=delegate_ss58, + call_hash=call_hash, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_reject_announcement_extrinsic.return_value + + +def test_remove_proxy_announcement(mocker, subtensor): + """Tests `remove_proxy_announcement` extrinsic call method.""" + # preps + wallet = mocker.Mock(spec=Wallet) + real_account_ss58 = mocker.Mock(spec=str) + call_hash = mocker.Mock(spec=str) + mocked_remove_announcement_extrinsic = mocker.patch.object( + subtensor_module, "remove_announcement_extrinsic" + ) + + # call + response = subtensor.remove_proxy_announcement( + wallet=wallet, + real_account_ss58=real_account_ss58, + call_hash=call_hash, + ) + + # asserts + mocked_remove_announcement_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=wallet, + real_account_ss58=real_account_ss58, + call_hash=call_hash, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_remove_announcement_extrinsic.return_value + + +def test_remove_proxies(mocker, subtensor): + """Tests `remove_proxies` extrinsic call method.""" + # preps + wallet = mocker.Mock(spec=Wallet) + mocked_remove_proxies_extrinsic = mocker.patch.object( + subtensor_module, "remove_proxies_extrinsic" + ) + + # call + response = subtensor.remove_proxies(wallet=wallet) + + # asserts + mocked_remove_proxies_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=wallet, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_remove_proxies_extrinsic.return_value + + +def test_remove_proxy(mocker, subtensor): + """Tests `remove_proxy` extrinsic call method.""" + # preps + wallet = mocker.Mock(spec=Wallet) + delegate_ss58 = mocker.Mock(spec=str) + proxy_type = mocker.Mock(spec=str) + delay = mocker.Mock(spec=int) + mocked_remove_proxy_extrinsic = mocker.patch.object( + subtensor_module, "remove_proxy_extrinsic" + ) + + # call + response = subtensor.remove_proxy( + wallet=wallet, + delegate_ss58=delegate_ss58, + proxy_type=proxy_type, + delay=delay, + ) + + # asserts + mocked_remove_proxy_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=wallet, + delegate_ss58=delegate_ss58, + proxy_type=proxy_type, + delay=delay, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_remove_proxy_extrinsic.return_value From 35f83f19037006db6fb3b9860496a78e01ad7984 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 10 Nov 2025 10:56:37 -0800 Subject: [PATCH 33/62] unit tests for extrinsics --- tests/unit_tests/extrinsics/test_proxy.py | 579 ++++++++++++++++++++++ tests/unit_tests/test_subtensor.py | 16 +- 2 files changed, 583 insertions(+), 12 deletions(-) create mode 100644 tests/unit_tests/extrinsics/test_proxy.py diff --git a/tests/unit_tests/extrinsics/test_proxy.py b/tests/unit_tests/extrinsics/test_proxy.py new file mode 100644 index 0000000000..13fff48a14 --- /dev/null +++ b/tests/unit_tests/extrinsics/test_proxy.py @@ -0,0 +1,579 @@ +import pytest + +from bittensor.core.extrinsics import proxy +from bittensor.core.types import ExtrinsicResponse +from scalecodec.types import GenericCall +from bittensor_wallet import Wallet + + +def test_add_proxy_extrinsic(subtensor, mocker): + """Verify that sync `add_proxy_extrinsic` method calls proper async method.""" + # Preps + wallet = mocker.MagicMock(spec=Wallet) + delegate_ss58 = mocker.MagicMock(spec=str) + proxy_type = mocker.MagicMock(spec=proxy.ProxyType) + delay = mocker.MagicMock(spec=int) + + mocked_normalize = mocker.patch.object(proxy.ProxyType, "normalize") + mocked_pallet_call = mocker.patch.object(proxy.Proxy, "add_proxy") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # Call + response = proxy.add_proxy_extrinsic( + subtensor=subtensor, + wallet=wallet, + delegate_ss58=delegate_ss58, + proxy_type=proxy_type, + delay=delay, + ) + + # Asserts + mocked_normalize.assert_called_once_with(proxy_type) + mocked_pallet_call.assert_called_once_with( + delegate=delegate_ss58, + proxy_type=mocked_normalize.return_value, + delay=delay, + ) + mocked_sign_and_send_extrinsic.assert_called_once_with( + call=mocked_pallet_call.return_value, + wallet=wallet, + raise_error=False, + period=None, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_sign_and_send_extrinsic.return_value + + +def test_remove_proxy_extrinsic(subtensor, mocker): + """Verify that sync `remove_proxy_extrinsic` method calls proper methods.""" + # Preps + wallet = mocker.MagicMock(spec=Wallet) + delegate_ss58 = mocker.MagicMock(spec=str) + proxy_type = mocker.MagicMock(spec=proxy.ProxyType) + delay = mocker.MagicMock(spec=int) + + mocked_normalize = mocker.patch.object(proxy.ProxyType, "normalize") + mocked_pallet_call = mocker.patch.object(proxy.Proxy, "remove_proxy") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # Call + response = proxy.remove_proxy_extrinsic( + subtensor=subtensor, + wallet=wallet, + delegate_ss58=delegate_ss58, + proxy_type=proxy_type, + delay=delay, + ) + + # Asserts + mocked_normalize.assert_called_once_with(proxy_type) + mocked_pallet_call.assert_called_once_with( + delegate=delegate_ss58, + proxy_type=mocked_normalize.return_value, + delay=delay, + ) + mocked_sign_and_send_extrinsic.assert_called_once_with( + call=mocked_pallet_call.return_value, + wallet=wallet, + raise_error=False, + period=None, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_sign_and_send_extrinsic.return_value + + +def test_remove_proxies_extrinsic(subtensor, mocker): + """Verify that sync `remove_proxies_extrinsic` method calls proper methods.""" + # Preps + wallet = mocker.MagicMock(spec=Wallet) + + mocked_pallet_call = mocker.patch.object(proxy.Proxy, "remove_proxies") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # Call + response = proxy.remove_proxies_extrinsic( + subtensor=subtensor, + wallet=wallet, + ) + + # Asserts + mocked_pallet_call.assert_called_once_with() + mocked_sign_and_send_extrinsic.assert_called_once_with( + call=mocked_pallet_call.return_value, + wallet=wallet, + raise_error=False, + period=None, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_sign_and_send_extrinsic.return_value + + +def test_create_pure_proxy_extrinsic(subtensor, mocker): + """Verify that sync `create_pure_proxy_extrinsic` method calls proper methods.""" + # Preps + wallet = mocker.MagicMock(spec=Wallet) + proxy_type = mocker.MagicMock(spec=proxy.ProxyType) + delay = mocker.MagicMock(spec=int) + index = mocker.MagicMock(spec=int) + + mocked_normalize = mocker.patch.object(proxy.ProxyType, "normalize") + mocked_pallet_call = mocker.patch.object(proxy.Proxy, "create_pure") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # Mock response with events + mock_response = mocker.MagicMock(spec=ExtrinsicResponse) + mock_response.success = True + mock_response.extrinsic_receipt = mocker.MagicMock() + mock_response.extrinsic_receipt.triggered_events = [ + { + "event_id": "PureCreated", + "attributes": { + "pure": "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty", + "who": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", + "proxy_type": "Any", + "disambiguation_index": 0, + }, + } + ] + mock_response.extrinsic_receipt.block_hash = mocker.MagicMock(spec=str) + mock_response.extrinsic_receipt.extrinsic_idx = 1 + mocked_sign_and_send_extrinsic.return_value = mock_response + + mocked_get_block_number = mocker.patch.object( + subtensor.substrate, "get_block_number" + ) + + # Call + response = proxy.create_pure_proxy_extrinsic( + subtensor=subtensor, + wallet=wallet, + proxy_type=proxy_type, + delay=delay, + index=index, + ) + + # Asserts + mocked_normalize.assert_called_once_with(proxy_type) + mocked_pallet_call.assert_called_once_with( + proxy_type=mocked_normalize.return_value, + delay=delay, + index=index, + ) + mocked_sign_and_send_extrinsic.assert_called_once_with( + call=mocked_pallet_call.return_value, + wallet=wallet, + raise_error=False, + period=None, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + mocked_get_block_number.assert_called_once() + assert response == mock_response + assert ( + response.data["pure_account"] + == "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + ) + assert ( + response.data["spawner"] == "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" + ) + assert response.data["height"] == mocked_get_block_number.return_value + assert response.data["ext_index"] == 1 + + +def test_kill_pure_proxy_extrinsic(subtensor, mocker): + """Verify that sync `kill_pure_proxy_extrinsic` method calls proper methods.""" + # Preps + wallet = mocker.MagicMock(spec=Wallet) + wallet.coldkey.ss58_address = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" + pure_proxy_ss58 = mocker.MagicMock(spec=str) + spawner = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" + proxy_type = mocker.MagicMock(spec=proxy.ProxyType) + index = mocker.MagicMock(spec=int) + height = mocker.MagicMock(spec=int) + ext_index = mocker.MagicMock(spec=int) + + mocked_normalize = mocker.patch.object(proxy.ProxyType, "normalize") + mocked_pallet_call = mocker.patch.object(proxy.Proxy, "kill_pure") + mocked_proxy_extrinsic = mocker.patch.object(proxy, "proxy_extrinsic") + + # Call + response = proxy.kill_pure_proxy_extrinsic( + subtensor=subtensor, + wallet=wallet, + pure_proxy_ss58=pure_proxy_ss58, + spawner=spawner, + proxy_type=proxy_type, + index=index, + height=height, + ext_index=ext_index, + ) + + # Asserts + mocked_normalize.assert_called_once_with(proxy_type) + mocked_pallet_call.assert_called_once_with( + spawner=spawner, + proxy_type=mocked_normalize.return_value, + index=index, + height=height, + ext_index=ext_index, + ) + mocked_proxy_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=wallet, + real_account_ss58=pure_proxy_ss58, + force_proxy_type=proxy.ProxyType.Any, + call=mocked_pallet_call.return_value, + period=None, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_proxy_extrinsic.return_value + + +def test_kill_pure_proxy_extrinsic_spawner_mismatch(subtensor, mocker): + """Verify that `kill_pure_proxy_extrinsic` returns error when spawner doesn't match wallet.""" + # Preps + wallet = mocker.MagicMock(spec=Wallet) + wallet.coldkey.ss58_address = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + pure_proxy_ss58 = mocker.MagicMock(spec=str) + spawner = ( + "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" # Different from wallet + ) + proxy_type = mocker.MagicMock(spec=proxy.ProxyType) + index = mocker.MagicMock(spec=int) + height = mocker.MagicMock(spec=int) + ext_index = mocker.MagicMock(spec=int) + + mocked_normalize = mocker.patch.object(proxy.ProxyType, "normalize") + + # Call + response = proxy.kill_pure_proxy_extrinsic( + subtensor=subtensor, + wallet=wallet, + pure_proxy_ss58=pure_proxy_ss58, + spawner=spawner, + proxy_type=proxy_type, + index=index, + height=height, + ext_index=ext_index, + ) + + # Asserts + mocked_normalize.assert_called_once_with(proxy_type) + assert response.success is False + assert "Spawner address" in response.message + + +def test_proxy_extrinsic(subtensor, mocker): + """Verify that sync `proxy_extrinsic` method calls proper methods.""" + # Preps + wallet = mocker.MagicMock(spec=Wallet) + real_account_ss58 = mocker.MagicMock(spec=str) + force_proxy_type = mocker.MagicMock(spec=str) + call = mocker.MagicMock(spec=GenericCall) + + mocked_normalize = mocker.patch.object(proxy.ProxyType, "normalize") + mocked_pallet_call = mocker.patch.object(proxy.Proxy, "proxy") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # Call + response = proxy.proxy_extrinsic( + subtensor=subtensor, + wallet=wallet, + real_account_ss58=real_account_ss58, + force_proxy_type=force_proxy_type, + call=call, + ) + + # Asserts + mocked_normalize.assert_called_once_with(force_proxy_type) + mocked_pallet_call.assert_called_once_with( + real=real_account_ss58, + force_proxy_type=mocked_normalize.return_value, + call=call, + ) + mocked_sign_and_send_extrinsic.assert_called_once_with( + call=mocked_pallet_call.return_value, + wallet=wallet, + raise_error=False, + period=None, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_sign_and_send_extrinsic.return_value + + +def test_proxy_extrinsic_with_none_force_proxy_type(subtensor, mocker): + """Verify that sync `proxy_extrinsic` method handles None force_proxy_type.""" + # Preps + wallet = mocker.MagicMock(spec=Wallet) + real_account_ss58 = mocker.MagicMock(spec=str) + force_proxy_type = None + call = mocker.MagicMock(spec=GenericCall) + + mocked_pallet_call = mocker.patch.object(proxy.Proxy, "proxy") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # Call + response = proxy.proxy_extrinsic( + subtensor=subtensor, + wallet=wallet, + real_account_ss58=real_account_ss58, + force_proxy_type=force_proxy_type, + call=call, + ) + + # Asserts + mocked_pallet_call.assert_called_once_with( + real=real_account_ss58, + force_proxy_type=None, + call=call, + ) + mocked_sign_and_send_extrinsic.assert_called_once_with( + call=mocked_pallet_call.return_value, + wallet=wallet, + raise_error=False, + period=None, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_sign_and_send_extrinsic.return_value + + +def test_proxy_announced_extrinsic(subtensor, mocker): + """Verify that sync `proxy_announced_extrinsic` method calls proper methods.""" + # Preps + wallet = mocker.MagicMock(spec=Wallet) + delegate_ss58 = mocker.MagicMock(spec=str) + real_account_ss58 = mocker.MagicMock(spec=str) + force_proxy_type = mocker.MagicMock(spec=str) + call = mocker.MagicMock(spec=GenericCall) + + mocked_normalize = mocker.patch.object(proxy.ProxyType, "normalize") + mocked_pallet_call = mocker.patch.object(proxy.Proxy, "proxy_announced") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # Call + response = proxy.proxy_announced_extrinsic( + subtensor=subtensor, + wallet=wallet, + delegate_ss58=delegate_ss58, + real_account_ss58=real_account_ss58, + force_proxy_type=force_proxy_type, + call=call, + ) + + # Asserts + mocked_normalize.assert_called_once_with(force_proxy_type) + mocked_pallet_call.assert_called_once_with( + delegate=delegate_ss58, + real=real_account_ss58, + force_proxy_type=mocked_normalize.return_value, + call=call, + ) + mocked_sign_and_send_extrinsic.assert_called_once_with( + call=mocked_pallet_call.return_value, + wallet=wallet, + raise_error=False, + period=None, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_sign_and_send_extrinsic.return_value + + +def test_proxy_announced_extrinsic_with_none_force_proxy_type(subtensor, mocker): + """Verify that sync `proxy_announced_extrinsic` method handles None force_proxy_type.""" + # Preps + wallet = mocker.MagicMock(spec=Wallet) + delegate_ss58 = mocker.MagicMock(spec=str) + real_account_ss58 = mocker.MagicMock(spec=str) + force_proxy_type = None + call = mocker.MagicMock(spec=GenericCall) + + mocked_pallet_call = mocker.patch.object(proxy.Proxy, "proxy_announced") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # Call + response = proxy.proxy_announced_extrinsic( + subtensor=subtensor, + wallet=wallet, + delegate_ss58=delegate_ss58, + real_account_ss58=real_account_ss58, + force_proxy_type=force_proxy_type, + call=call, + ) + + # Asserts + mocked_pallet_call.assert_called_once_with( + delegate=delegate_ss58, + real=real_account_ss58, + force_proxy_type=None, + call=call, + ) + mocked_sign_and_send_extrinsic.assert_called_once_with( + call=mocked_pallet_call.return_value, + wallet=wallet, + raise_error=False, + period=None, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_sign_and_send_extrinsic.return_value + + +def test_announce_extrinsic(subtensor, mocker): + """Verify that sync `announce_extrinsic` method calls proper methods.""" + # Preps + wallet = mocker.MagicMock(spec=Wallet) + real_account_ss58 = mocker.MagicMock(spec=str) + call_hash = mocker.MagicMock(spec=str) + + mocked_pallet_call = mocker.patch.object(proxy.Proxy, "announce") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # Call + response = proxy.announce_extrinsic( + subtensor=subtensor, + wallet=wallet, + real_account_ss58=real_account_ss58, + call_hash=call_hash, + ) + + # Asserts + mocked_pallet_call.assert_called_once_with( + real=real_account_ss58, + call_hash=call_hash, + ) + mocked_sign_and_send_extrinsic.assert_called_once_with( + call=mocked_pallet_call.return_value, + wallet=wallet, + raise_error=False, + period=None, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_sign_and_send_extrinsic.return_value + + +def test_reject_announcement_extrinsic(subtensor, mocker): + """Verify that sync `reject_announcement_extrinsic` method calls proper methods.""" + # Preps + wallet = mocker.MagicMock(spec=Wallet) + delegate_ss58 = mocker.MagicMock(spec=str) + call_hash = mocker.MagicMock(spec=str) + + mocked_pallet_call = mocker.patch.object(proxy.Proxy, "reject_announcement") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # Call + response = proxy.reject_announcement_extrinsic( + subtensor=subtensor, + wallet=wallet, + delegate_ss58=delegate_ss58, + call_hash=call_hash, + ) + + # Asserts + mocked_pallet_call.assert_called_once_with( + delegate=delegate_ss58, + call_hash=call_hash, + ) + mocked_sign_and_send_extrinsic.assert_called_once_with( + call=mocked_pallet_call.return_value, + wallet=wallet, + raise_error=False, + period=None, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_sign_and_send_extrinsic.return_value + + +def test_remove_announcement_extrinsic(subtensor, mocker): + """Verify that sync `remove_announcement_extrinsic` method calls proper methods.""" + # Preps + wallet = mocker.MagicMock(spec=Wallet) + real_account_ss58 = mocker.MagicMock(spec=str) + call_hash = mocker.MagicMock(spec=str) + + mocked_pallet_call = mocker.patch.object(proxy.Proxy, "remove_announcement") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # Call + response = proxy.remove_announcement_extrinsic( + subtensor=subtensor, + wallet=wallet, + real_account_ss58=real_account_ss58, + call_hash=call_hash, + ) + + # Asserts + mocked_pallet_call.assert_called_once_with( + real=real_account_ss58, + call_hash=call_hash, + ) + mocked_sign_and_send_extrinsic.assert_called_once_with( + call=mocked_pallet_call.return_value, + wallet=wallet, + raise_error=False, + period=None, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_sign_and_send_extrinsic.return_value + + +def test_poke_deposit_extrinsic(subtensor, mocker): + """Verify that sync `poke_deposit_extrinsic` method calls proper methods.""" + # Preps + wallet = mocker.MagicMock(spec=Wallet) + + mocked_pallet_call = mocker.patch.object(proxy.Proxy, "poke_deposit") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # Call + response = proxy.poke_deposit_extrinsic( + subtensor=subtensor, + wallet=wallet, + ) + + # Asserts + mocked_pallet_call.assert_called_once_with() + mocked_sign_and_send_extrinsic.assert_called_once_with( + call=mocked_pallet_call.return_value, + wallet=wallet, + raise_error=False, + period=None, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_sign_and_send_extrinsic.return_value diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index c57840776b..1faeb06df5 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -5512,9 +5512,7 @@ def test_get_proxy_constants_success(subtensor, mocker): mocked_query_constant = mocker.patch.object( subtensor, "query_constant", - side_effect=[ - mocker.Mock(value=value) for value in fake_constants.values() - ], + side_effect=[mocker.Mock(value=value) for value in fake_constants.values()], ) mocked_from_dict = mocker.patch.object( subtensor_module.ProxyConstants, @@ -5552,9 +5550,7 @@ def test_get_proxy_constants_as_dict(subtensor, mocker): mocked_query_constant = mocker.patch.object( subtensor, "query_constant", - side_effect=[ - mocker.Mock(value=value) for value in fake_constants.values() - ], + side_effect=[mocker.Mock(value=value) for value in fake_constants.values()], ) mocked_proxy_constants = mocker.Mock() mocked_from_dict = mocker.patch.object( @@ -5591,9 +5587,7 @@ def test_get_proxy_constants_specific_constants(subtensor, mocker): mocked_query_constant = mocker.patch.object( subtensor, "query_constant", - side_effect=[ - mocker.Mock(value=value) for value in fake_constants.values() - ], + side_effect=[mocker.Mock(value=value) for value in fake_constants.values()], ) mocked_from_dict = mocker.patch.object( subtensor_module.ProxyConstants, @@ -5787,9 +5781,7 @@ def test_proxy(mocker, subtensor): real_account_ss58 = mocker.Mock(spec=str) force_proxy_type = mocker.Mock(spec=str) call = mocker.Mock(spec=GenericCall) - mocked_proxy_extrinsic = mocker.patch.object( - subtensor_module, "proxy_extrinsic" - ) + mocked_proxy_extrinsic = mocker.patch.object(subtensor_module, "proxy_extrinsic") # call response = subtensor.proxy( From de355f8011a01db99e6964491991f7a5fd681678 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 10 Nov 2025 11:17:16 -0800 Subject: [PATCH 34/62] add async extrinsics --- bittensor/core/extrinsics/asyncex/proxy.py | 853 +++++++++++++++++++++ 1 file changed, 853 insertions(+) create mode 100644 bittensor/core/extrinsics/asyncex/proxy.py diff --git a/bittensor/core/extrinsics/asyncex/proxy.py b/bittensor/core/extrinsics/asyncex/proxy.py new file mode 100644 index 0000000000..37b1b5d617 --- /dev/null +++ b/bittensor/core/extrinsics/asyncex/proxy.py @@ -0,0 +1,853 @@ +from typing import TYPE_CHECKING, Optional, Union + +from bittensor.core.chain_data.proxy import ProxyType +from bittensor.core.extrinsics.pallets import Proxy +from bittensor.core.types import ExtrinsicResponse +from bittensor.utils.btlogging import logging + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from scalecodec.types import GenericCall + + from bittensor.core.async_subtensor import AsyncSubtensor + + +async def add_proxy_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + delegate_ss58: str, + proxy_type: Union[str, ProxyType], + delay: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> ExtrinsicResponse: + """ + Adds a proxy relationship. + + Parameters: + subtensor: Subtensor instance with the connection to the chain. + wallet: Bittensor wallet object. + delegate_ss58: The SS58 address of the delegate proxy account. + proxy_type: The type of proxy permissions (e.g., "Any", "NonTransfer", "Governance", "Staking"). Can be a + string or ProxyType enum value. + delay: The number of blocks before the proxy can be used. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + proxy_type_str = ProxyType.normalize(proxy_type) + + logging.debug( + f"Adding proxy: delegate=[blue]{delegate_ss58}[/blue], " + f"type=[blue]{proxy_type_str}[/blue], delay=[blue]{delay}[/blue] " + f"on [blue]{subtensor.network}[/blue]." + ) + + call = await Proxy(subtensor).add_proxy( + delegate=delegate_ss58, + proxy_type=proxy_type_str, + delay=delay, + ) + + response = 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, + ) + + if response.success: + logging.debug("[green]Proxy added successfully.[/green]") + else: + logging.error(f"[red]{response.message}[/red]") + + return response + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + +async def remove_proxy_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + delegate_ss58: str, + proxy_type: Union[str, ProxyType], + delay: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> ExtrinsicResponse: + """ + Removes a proxy relationship. + + Parameters: + subtensor: Subtensor instance with the connection to the chain. + wallet: Bittensor wallet object. + delegate_ss58: The SS58 address of the delegate proxy account to remove. + proxy_type: The type of proxy permissions to remove. Can be a string or ProxyType enum value. + delay: The number of blocks before the proxy removal takes effect. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + proxy_type_str = ProxyType.normalize(proxy_type) + + logging.debug( + f"Removing proxy: delegate=[blue]{delegate_ss58}[/blue], " + f"type=[blue]{proxy_type_str}[/blue], delay=[blue]{delay}[/blue] " + f"on [blue]{subtensor.network}[/blue]." + ) + + call = await Proxy(subtensor).remove_proxy( + delegate=delegate_ss58, + proxy_type=proxy_type_str, + delay=delay, + ) + + response = 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, + ) + + if response.success: + logging.debug("[green]Proxy removed successfully.[/green]") + else: + logging.error(f"[red]{response.message}[/red]") + + return response + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + +async def remove_proxies_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> ExtrinsicResponse: + """ + Removes all proxy relationships for the account. + + This removes all proxy relationships in a single call, which is more efficient than removing them one by one. The + deposit for all proxies will be returned. + + Parameters: + subtensor: Subtensor instance with the connection to the chain. + wallet: Bittensor wallet object (the account whose proxies will be removed). + period: The number of blocks during which the transaction will remain valid after it's submitted. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + logging.debug( + f"Removing all proxies for account [blue]{wallet.coldkey.ss58_address}[/blue] " + f"on [blue]{subtensor.network}[/blue]." + ) + + call = await Proxy(subtensor).remove_proxies() + + response = 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, + ) + + if response.success: + logging.debug("[green]All proxies removed successfully.[/green]") + else: + logging.error(f"[red]{response.message}[/red]") + + return response + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + +async def create_pure_proxy_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + proxy_type: Union[str, ProxyType], + delay: int, + index: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> ExtrinsicResponse: + """ + Creates a pure proxy account. + + Parameters: + subtensor: Subtensor instance with the connection to the chain. + wallet: Bittensor wallet object. + proxy_type: The type of proxy permissions for the pure proxy. Can be a string or ProxyType enum value. + delay: The number of blocks before the pure proxy can be used. + index: The index to use for generating the pure proxy account address. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + proxy_type_str = ProxyType.normalize(proxy_type) + + logging.debug( + f"Creating pure proxy: type=[blue]{proxy_type_str}[/blue], " + f"delay=[blue]{delay}[/blue], index=[blue]{index}[/blue] " + f"on [blue]{subtensor.network}[/blue]." + ) + + call = await Proxy(subtensor).create_pure( + proxy_type=proxy_type_str, + delay=delay, + index=index, + ) + + response = 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, + ) + + if response.success: + logging.debug("[green]Pure proxy created successfully.[/green]") + + # Extract pure proxy address from PureCreated triggered event + for event in response.extrinsic_receipt.triggered_events: + if event.get("event_id") == "PureCreated": + # Event structure: PureProxyCreated { disambiguation_index, proxy_type, pure, who } + attributes = event.get("attributes", []) + if attributes: + response.data = { + "pure_account": attributes.get("pure"), + "spawner": attributes.get("who"), + "proxy_type": attributes.get("proxy_type"), + "index": attributes.get("disambiguation_index"), + "height": await subtensor.substrate.get_block_number( + response.extrinsic_receipt.block_hash + ), + "ext_index": response.extrinsic_receipt.extrinsic_idx, + } + break + else: + logging.error(f"[red]{response.message}[/red]") + + return response + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + +async def kill_pure_proxy_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + pure_proxy_ss58: str, + spawner: str, + proxy_type: Union[str, "ProxyType"], + index: int, + height: int, + ext_index: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> ExtrinsicResponse: + """ + Kills (removes) a pure proxy account. + + This method removes a pure proxy account that was previously created via `create_pure_proxy()`. + The `kill_pure` call must be executed through the pure proxy account itself, with the spawner + acting as an "Any" proxy. This method automatically handles this by executing the call via + `proxy()`. + + Parameters: + subtensor: Subtensor instance with the connection to the chain. + wallet: Bittensor wallet object. The wallet.coldkey.ss58_address must be the spawner of + the pure proxy (the account that created it via `create_pure_proxy()`). The spawner + must have an "Any" proxy relationship with the pure proxy. + pure_proxy_ss58: The SS58 address of the pure proxy account to be killed. This is the + address that was returned in the `create_pure_proxy()` response. + spawner: The SS58 address of the spawner account (the account that originally created + the pure proxy via `create_pure_proxy()`). This should match wallet.coldkey.ss58_address. + proxy_type: The type of proxy permissions. Can be a string or ProxyType enum value. Must + match the proxy_type used when creating the pure proxy. + index: The disambiguation index originally passed to `create_pure()`. + height: The block height at which the pure proxy was created. + ext_index: The extrinsic index at which the pure proxy was created. + period: The number of blocks during which the transaction will remain valid after it's + submitted. If the transaction is not included in a block within that number of blocks, + it will expire and be rejected. You can think of it as an expiration date for the + transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + The `kill_pure` call must be executed through the pure proxy account itself, with the + spawner acting as an "Any" proxy. This method automatically handles this by executing + the call via `proxy()`. The spawner must have an "Any" proxy relationship with the pure + proxy for this to work. + + Example: + # After creating a pure proxy + create_response = await subtensor.proxies.create_pure_proxy( + wallet=spawner_wallet, + proxy_type=ProxyType.Any, + delay=0, + index=0, + ) + pure_proxy_ss58 = create_response.data["pure_account"] + spawner = create_response.data["spawner"] + height = create_response.data["height"] + ext_index = create_response.data["ext_index"] + + # Kill the pure proxy + kill_response = await subtensor.proxies.kill_pure_proxy( + wallet=spawner_wallet, + pure_proxy_ss58=pure_proxy_ss58, + spawner=spawner, + proxy_type=ProxyType.Any, + index=0, + height=height, + ext_index=ext_index, + ) + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + proxy_type_str = ProxyType.normalize(proxy_type) + + # Validate that spawner matches wallet + if wallet.coldkey.ss58_address != spawner: + error_msg = ( + f"Spawner address {spawner} does not match wallet address " + f"{wallet.coldkey.ss58_address}" + ) + if raise_error: + raise ValueError(error_msg) + return ExtrinsicResponse(False, error_msg) + + logging.debug( + f"Killing pure proxy: pure=[blue]{pure_proxy_ss58}[/blue], " + f"spawner=[blue]{spawner}[/blue], type=[blue]{proxy_type_str}[/blue], " + f"index=[blue]{index}[/blue], height=[blue]{height}[/blue], " + f"ext_index=[blue]{ext_index}[/blue] on [blue]{subtensor.network}[/blue]." + ) + + # Create the kill_pure call + kill_pure_call = await Proxy(subtensor).kill_pure( + spawner=spawner, + proxy_type=proxy_type_str, + index=index, + height=height, + ext_index=ext_index, + ) + + # Execute kill_pure through proxy() where: + # - wallet (spawner) signs the transaction + # - real_account_ss58 (pure_proxy_ss58) is the origin (pure proxy account) + # - force_proxy_type=Any (spawner acts as Any proxy for pure proxy) + response = await proxy_extrinsic( + subtensor=subtensor, + wallet=wallet, + real_account_ss58=pure_proxy_ss58, + force_proxy_type=ProxyType.Any, + call=kill_pure_call, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if response.success: + logging.debug("[green]Pure proxy killed successfully.[/green]") + else: + logging.error(f"[red]{response.message}[/red]") + + return response + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + +async def proxy_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + real_account_ss58: str, + force_proxy_type: Optional[Union[str, ProxyType]], + call: "GenericCall", + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> ExtrinsicResponse: + """ + Executes a call on behalf of the real account through a proxy. + + Parameters: + subtensor: Subtensor instance with the connection to the chain. + wallet: Bittensor wallet object (should be the proxy account wallet). + real_account_ss58: The SS58 address of the real account on whose behalf the call is being made. + force_proxy_type: The type of proxy to use for the call. If None, any proxy type can be used. Otherwise, must + match one of the allowed proxy types. Can be a string or ProxyType enum value. + call: The inner call to be executed on behalf of the real account. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + force_proxy_type_str = ( + ProxyType.normalize(force_proxy_type) + if force_proxy_type is not None + else None + ) + + logging.debug( + f"Executing proxy call: real=[blue]{real_account_ss58}[/blue], " + f"force_type=[blue]{force_proxy_type_str}[/blue] " + f"on [blue]{subtensor.network}[/blue]." + ) + + proxy_call = await Proxy(subtensor).proxy( + real=real_account_ss58, + force_proxy_type=force_proxy_type_str, + call=call, + ) + + response = await subtensor.sign_and_send_extrinsic( + call=proxy_call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + raise_error=raise_error, + ) + + if response.success: + logging.debug("[green]Proxy call executed successfully.[/green]") + else: + logging.error(f"[red]{response.message}[/red]") + + return response + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + +async def proxy_announced_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + delegate_ss58: str, + real_account_ss58: str, + force_proxy_type: Optional[Union[str, ProxyType]], + call: "GenericCall", + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> ExtrinsicResponse: + """ + Executes an announced call on behalf of the real account through a proxy. + + This extrinsic executes a call that was previously announced via `announce_extrinsic`. The call must match the + call_hash that was announced, and the delay period must have passed. + + Parameters: + subtensor: Subtensor instance with the connection to the chain. + wallet: Bittensor wallet object (should be the proxy account wallet that made the announcement). + delegate_ss58: The SS58 address of the delegate proxy account that made the announcement. + real_account_ss58: The SS58 address of the real account on whose behalf the call will be made. + force_proxy_type: The type of proxy to use for the call. If None, any proxy type can be used. Otherwise, must + match one of the allowed proxy types. Can be a string or ProxyType enum value. + call: The inner call to be executed on behalf of the real account (must match the announced call_hash). + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + force_proxy_type_str = ( + ProxyType.normalize(force_proxy_type) + if force_proxy_type is not None + else None + ) + + logging.debug( + f"Executing announced proxy call: delegate=[blue]{delegate_ss58}[/blue], " + f"real=[blue]{real_account_ss58}[/blue], force_type=[blue]{force_proxy_type_str}[/blue] " + f"on [blue]{subtensor.network}[/blue]." + ) + + proxy_call = await Proxy(subtensor).proxy_announced( + delegate=delegate_ss58, + real=real_account_ss58, + force_proxy_type=force_proxy_type_str, + call=call, + ) + + response = await subtensor.sign_and_send_extrinsic( + call=proxy_call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + raise_error=raise_error, + ) + + if response.success: + logging.debug("[green]Announced proxy call executed successfully.[/green]") + else: + logging.error(f"[red]{response.message}[/red]") + + return response + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + +async def announce_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + real_account_ss58: str, + call_hash: str, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> ExtrinsicResponse: + """ + Announces a future call that will be executed through a proxy. + + Parameters: + subtensor: Subtensor instance with the connection to the chain. + wallet: Bittensor wallet object (should be the proxy account wallet). + real_account_ss58: The SS58 address of the real account on whose behalf the call will be made. + call_hash: The hash of the call that will be executed in the future. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + logging.debug( + f"Announcing proxy call: real=[blue]{real_account_ss58}[/blue], " + f"call_hash=[blue]{call_hash}[/blue] " + f"on [blue]{subtensor.network}[/blue]." + ) + + call = await Proxy(subtensor).announce( + real=real_account_ss58, + call_hash=call_hash, + ) + + response = 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, + ) + + if response.success: + logging.debug("[green]Proxy call announced successfully.[/green]") + else: + logging.error(f"[red]{response.message}[/red]") + + return response + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + +async def reject_announcement_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + delegate_ss58: str, + call_hash: str, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> ExtrinsicResponse: + """ + Rejects an announcement made by a proxy delegate. + + This extrinsic allows the real account to reject an announcement made by a proxy delegate. This prevents the + announced call from being executed. + + Parameters: + subtensor: Subtensor instance with the connection to the chain. + wallet: Bittensor wallet object (should be the real account wallet). + delegate_ss58: The SS58 address of the delegate proxy account whose announcement is being rejected. + call_hash: The hash of the call that was announced and is now being rejected. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + logging.debug( + f"Rejecting announcement: delegate=[blue]{delegate_ss58}[/blue], " + f"call_hash=[blue]{call_hash}[/blue] " + f"on [blue]{subtensor.network}[/blue]." + ) + + call = await Proxy(subtensor).reject_announcement( + delegate=delegate_ss58, + call_hash=call_hash, + ) + + response = 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, + ) + + if response.success: + logging.debug("[green]Announcement rejected successfully.[/green]") + else: + logging.error(f"[red]{response.message}[/red]") + + return response + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + +async def remove_announcement_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + real_account_ss58: str, + call_hash: str, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> ExtrinsicResponse: + """ + Removes an announcement made by a proxy account. + + This extrinsic allows the proxy account to remove its own announcement before it is executed or rejected. This frees + up the announcement deposit. + + Parameters: + subtensor: Subtensor instance with the connection to the chain. + wallet: Bittensor wallet object (should be the proxy account wallet that made the announcement). + real_account_ss58: The SS58 address of the real account on whose behalf the call was announced. + call_hash: The hash of the call that was announced and is now being removed. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + logging.debug( + f"Removing announcement: real=[blue]{real_account_ss58}[/blue], " + f"call_hash=[blue]{call_hash}[/blue] " + f"on [blue]{subtensor.network}[/blue]." + ) + + call = await Proxy(subtensor).remove_announcement( + real=real_account_ss58, + call_hash=call_hash, + ) + + response = 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, + ) + + if response.success: + logging.debug("[green]Announcement removed successfully.[/green]") + else: + logging.error(f"[red]{response.message}[/red]") + + return response + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) + + +async def poke_deposit_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> ExtrinsicResponse: + """ + Adjusts deposits made for proxies and announcements based on current values. + + This can be used by accounts to possibly lower their locked amount. The function automatically recalculates deposits + for both proxy relationships and announcements for the signing account. The transaction fee is waived if the deposit + amount has changed. + + Parameters: + subtensor: Subtensor instance with the connection to the chain. + wallet: Bittensor wallet object (the account whose deposits will be adjusted). + period: The number of blocks during which the transaction will remain valid after it's submitted. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + """ + try: + if not ( + unlocked := ExtrinsicResponse.unlock_wallet(wallet, raise_error) + ).success: + return unlocked + + logging.debug( + f"Poking deposit for account [blue]{wallet.coldkey.ss58_address}[/blue] " + f"on [blue]{subtensor.network}[/blue]." + ) + + call = await Proxy(subtensor).poke_deposit() + + response = 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, + ) + + if response.success: + logging.debug("[green]Deposit poked successfully.[/green]") + else: + logging.error(f"[red]{response.message}[/red]") + + return response + + except Exception as error: + return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) From 1ee43c03f2ceaaecc3ce3753dec049a65eaa09d4 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 10 Nov 2025 11:35:41 -0800 Subject: [PATCH 35/62] import --- bittensor/core/subtensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index a54202456b..37b5a16817 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -12,7 +12,6 @@ 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 from bittensor.core.chain_data import ( CrowdloanInfo, @@ -23,6 +22,7 @@ MetagraphInfo, NeuronInfo, NeuronInfoLite, + ProposalVoteData, ProxyAnnouncementInfo, ProxyInfo, ProxyConstants, @@ -2251,8 +2251,8 @@ def get_proxies(self, block: Optional[int] = None) -> dict[str, list[ProxyInfo]] block: The blockchain block number for the query. If None, queries the latest block. Returns: - Dictionary mapping real account SS58 addresses to lists of ProxyInfo objects. Each ProxyInfo - contains the delegate address, proxy type, and delay for that proxy relationship. + Dictionary mapping real account SS58 addresses to lists of ProxyInfo objects. Each ProxyInfo contains the + delegate address, proxy type, and delay for that proxy relationship. Note: This method queries all proxy relationships on the chain, which may be resource-intensive for large From 70e375d052e6a9bd417165bf2e579e195fa14892 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 10 Nov 2025 11:35:53 -0800 Subject: [PATCH 36/62] add async query methods --- bittensor/core/async_subtensor.py | 225 ++++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 914df7475b..e92011522d 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -22,6 +22,10 @@ NeuronInfoLite, NeuronInfo, ProposalVoteData, + ProxyAnnouncementInfo, + ProxyInfo, + ProxyConstants, + ProxyType, SelectiveMetagraphIndex, SimSwapResult, StakeInfo, @@ -67,6 +71,19 @@ swap_stake_extrinsic, move_stake_extrinsic, ) +from bittensor.core.extrinsics.asyncex.proxy import ( + add_proxy_extrinsic, + announce_extrinsic, + create_pure_proxy_extrinsic, + kill_pure_proxy_extrinsic, + poke_deposit_extrinsic, + proxy_announced_extrinsic, + proxy_extrinsic, + reject_announcement_extrinsic, + remove_announcement_extrinsic, + remove_proxy_extrinsic, + remove_proxies_extrinsic, +) from bittensor.core.extrinsics.asyncex.registration import ( burned_register_extrinsic, register_extrinsic, @@ -3037,6 +3054,214 @@ async def get_parents( return [] + async def get_proxies( + self, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> dict[str, list[ProxyInfo]]: + """ + Retrieves all proxy relationships from the chain. + + This method queries the Proxy.Proxies storage map across all accounts and returns a dictionary mapping each real + account (delegator) to its list of proxy relationships. + + Parameters: + block: The blockchain block number for the query. If None, queries the latest block. + block_hash: The hash of the block 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: + Dictionary mapping real account SS58 addresses to lists of ProxyInfo objects. Each ProxyInfo contains the + delegate address, proxy type, and delay for that proxy relationship. + + Note: + This method queries all proxy relationships on the chain, which may be resource-intensive for large + networks. Consider using `get_proxies_for_real_account()` for querying specific accounts. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + query_map = await self.substrate.query_map( + module="Proxy", + storage_function="Proxies", + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + proxies = {} + if query_map.records: + async for record in query_map: + real_account, proxy_list = ProxyInfo.from_query_map_record(record) + proxies[real_account] = proxy_list + return proxies + + async def get_proxies_for_real_account( + self, + real_account_ss58: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> tuple[list[ProxyInfo], Balance]: + """ + Returns proxy/ies associated with the provided real account. + + This method queries the Proxy.Proxies storage for a specific real account and returns all proxy relationships + where this real account is the delegator. It also returns the deposit amount reserved for these proxies. + + Parameters: + real_account_ss58: SS58 address of the real account (delegator) whose proxies to retrieve. + block: The blockchain block number for the query. + block_hash: The hash of the block 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: + Tuple containing: + - List of ProxyInfo objects representing all proxy relationships for the real account. Each ProxyInfo + contains delegate address, proxy type, and delay. + - Balance object representing the reserved deposit amount for these proxies. This deposit is held as + long as the proxy relationships exist and is returned when proxies are removed. + + Note: + If the account has no proxies, returns an empty list and a zero balance. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + query = await self.substrate.query( + module="Proxy", + storage_function="Proxies", + params=[real_account_ss58], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + return ProxyInfo.from_query(query) + + async def get_proxy_announcement( + self, + delegate_account_ss58: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> list[ProxyAnnouncementInfo]: + """ + Retrieves proxy announcements for a specific delegate account. + + This method queries the Proxy.Announcements storage for announcements made by the given delegate proxy account. + Announcements allow a proxy to declare its intention to execute a call on behalf of a real account after a delay + period. + + Parameters: + delegate_account_ss58: SS58 address of the delegate proxy account whose announcements to retrieve. + block: The blockchain block number for the query. If None, queries the latest block. + block_hash: The hash of the block 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: + List of ProxyAnnouncementInfo objects. Each object contains the real account address, call hash, and block + height at which the announcement was made. + + Note: + If the delegate has no announcements, returns an empty list. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + query = await self.substrate.query( + module="Proxy", + storage_function="Announcements", + params=[delegate_account_ss58], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + return ProxyAnnouncementInfo.from_dict(query.value[0]) + + async def get_proxy_announcements( + self, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> dict[str, list[ProxyAnnouncementInfo]]: + """ + Retrieves all proxy announcements from the chain. + + This method queries the Proxy.Announcements storage map across all delegate accounts and returns a dictionary + mapping each delegate to its list of pending announcements. + + Parameters: + block: The blockchain block number for the query. If None, queries the latest block. + block_hash: The hash of the block 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: + Dictionary mapping delegate account SS58 addresses to lists of ProxyAnnouncementInfo objects. + Each ProxyAnnouncementInfo contains the real account address, call hash, and block height. + + Note: + This method queries all announcements on the chain, which may be resource-intensive for large networks. + Consider using `get_proxy_announcement()` for querying specific delegates. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + query_map = await self.substrate.query_map( + module="Proxy", + storage_function="Announcements", + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + announcements = {} + if query_map.records: + async for record in query_map: + delegate, proxy_list = ProxyAnnouncementInfo.from_query_map_record(record) + announcements[delegate] = proxy_list + return announcements + + async def get_proxy_constants( + self, + constants: Optional[list[str]] = None, + as_dict: bool = False, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Union["ProxyConstants", dict]: + """ + Fetches runtime configuration constants from the `Proxy` pallet. + + This method retrieves on-chain configuration constants that define deposit requirements, proxy limits, and + announcement constraints for the Proxy pallet. These constants govern how proxy accounts operate within the + Subtensor network. + + Parameters: + constants: Optional list of specific constant names to fetch. If omitted, all constants defined in + `ProxyConstants.constants_names()` are queried. Valid constant names include: "AnnouncementDepositBase", + "AnnouncementDepositFactor", "MaxProxies", "MaxPending", "ProxyDepositBase", "ProxyDepositFactor". + as_dict: If True, returns the constants as a dictionary instead of a `ProxyConstants` object. + block: The blockchain block number for the query. If None, queries the latest block. + block_hash: The hash of the block 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: + If `as_dict` is False: ProxyConstants object containing all requested constants. + If `as_dict` is True: Dictionary mapping constant names to their values (Balance objects for deposit + constants, integers for limit constants). + + Note: + All Balance amounts are returned in RAO. Constants reflect the current chain configuration at the specified + block. + """ + result = {} + const_names = constants or ProxyConstants.constants_names() + + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + for const_name in const_names: + query = await self.query_constant( + module_name="Proxy", + constant_name=const_name, + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + + if query is not None: + result[const_name] = query.value + + proxy_constants = ProxyConstants.from_dict(result) + + return proxy_constants.to_dict() if as_dict else proxy_constants + async def get_revealed_commitment( self, netuid: int, From fea81c67c5438c09900f698d87293d0315ceee9c Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 10 Nov 2025 11:41:57 -0800 Subject: [PATCH 37/62] add async extrinsic call methods --- bittensor/core/async_subtensor.py | 536 +++++++++++++++++++++++++++++- 1 file changed, 535 insertions(+), 1 deletion(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index e92011522d..cb7a870345 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -3205,7 +3205,9 @@ async def get_proxy_announcements( announcements = {} if query_map.records: async for record in query_map: - delegate, proxy_list = ProxyAnnouncementInfo.from_query_map_record(record) + delegate, proxy_list = ProxyAnnouncementInfo.from_query_map_record( + record + ) announcements[delegate] = proxy_list return announcements @@ -5522,6 +5524,102 @@ async def add_stake_multiple( wait_for_finalization=wait_for_finalization, ) + async def add_proxy( + self, + wallet: "Wallet", + delegate_ss58: str, + proxy_type: Union[str, "ProxyType"], + delay: int, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> ExtrinsicResponse: + """ + Adds a proxy relationship. + + This method creates a proxy relationship where the delegate can execute calls on behalf of the real account (the + wallet owner) with restrictions defined by the proxy type and a delay period. A deposit is required and held as + long as the proxy relationship exists. + + Parameters: + wallet: Bittensor wallet object. + delegate_ss58: The SS58 address of the delegate proxy account. + proxy_type: The type of proxy permissions (e.g., "Any", "NonTransfer", "Governance", "Staking"). Can be a + string or ProxyType enum value. + delay: The number of blocks before the proxy can be used. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + A deposit is required when adding a proxy. The deposit amount is determined by runtime constants and is + returned when the proxy is removed. Use `get_proxy_constants()` to check current deposit requirements. + """ + return await add_proxy_extrinsic( + subtensor=self, + wallet=wallet, + delegate_ss58=delegate_ss58, + proxy_type=proxy_type, + delay=delay, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + async def announce_proxy( + self, + wallet: "Wallet", + real_account_ss58: str, + call_hash: str, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> ExtrinsicResponse: + """ + Announces a future call that will be executed through a proxy. + + This method allows a proxy account to declare its intention to execute a specific call on behalf of a real + account after a delay period. The real account can review and either approve (via `proxy_announced()`) or reject + (via `reject_proxy_announcement()`) the announcement. + + Parameters: + wallet: Bittensor wallet object (should be the proxy account wallet). + real_account_ss58: The SS58 address of the real account on whose behalf the call will be made. + call_hash: The hash of the call that will be executed in the future. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + A deposit is required when making an announcement. The deposit is returned when the announcement is + executed, rejected, or removed. The announcement can be executed after the delay period has passed. + """ + return await announce_extrinsic( + subtensor=self, + wallet=wallet, + real_account_ss58=real_account_ss58, + call_hash=call_hash, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + async def burned_register( self, wallet: "Wallet", @@ -5775,6 +5873,56 @@ async def create_crowdloan( wait_for_finalization=wait_for_finalization, ) + async def create_pure_proxy( + self, + wallet: "Wallet", + proxy_type: Union[str, "ProxyType"], + delay: int, + index: int, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> ExtrinsicResponse: + """ + Creates a pure proxy account. + + A pure proxy is a keyless account that can only be controlled through proxy relationships. Unlike regular + proxies, pure proxies do not have their own private keys, making them more secure for certain use cases. The + pure proxy address is deterministically generated based on the spawner account, proxy type, delay, and index. + + Parameters: + wallet: Bittensor wallet object. + proxy_type: The type of proxy permissions for the pure proxy. Can be a string or ProxyType enum value. + delay: The number of blocks before the pure proxy can be used. + index: The index to use for generating the pure proxy account address. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + The pure proxy account address can be extracted from the "PureCreated" event in the response. Store the + spawner address, proxy_type, index, height, and ext_index as they are required to kill the pure proxy later + via `kill_pure_proxy()`. + """ + return await create_pure_proxy_extrinsic( + subtensor=self, + wallet=wallet, + proxy_type=proxy_type, + delay=delay, + index=index, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + async def dissolve_crowdloan( self, wallet: "Wallet", @@ -5854,6 +6002,70 @@ async def finalize_crowdloan( wait_for_finalization=wait_for_finalization, ) + async def kill_pure_proxy( + self, + wallet: "Wallet", + pure_proxy_ss58: str, + spawner: str, + proxy_type: Union[str, "ProxyType"], + index: int, + height: int, + ext_index: int, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> ExtrinsicResponse: + """ + Kills (removes) a pure proxy account. + + This method removes a pure proxy account that was previously created via `create_pure_proxy()`. The `kill_pure` + call must be executed through the pure proxy account itself, with the spawner acting as an "Any" proxy. This + method automatically handles this by executing the call via `proxy()`. + + Parameters: + wallet: Bittensor wallet object. The wallet.coldkey.ss58_address must be the spawner of the pure proxy (the + account that created it via `create_pure_proxy()`). The spawner must have an "Any" proxy relationship + with the pure proxy. + pure_proxy_ss58: The SS58 address of the pure proxy account to be killed. This is the address that was + returned in the `create_pure_proxy()` response. + spawner: The SS58 address of the spawner account (the account that originally created the pure proxy via + `create_pure_proxy()`). This should match wallet.coldkey.ss58_address. + proxy_type: The type of proxy permissions. Can be a string or ProxyType enum value. Must match the + proxy_type used when creating the pure proxy. + index: The disambiguation index originally passed to `create_pure()`. + height: The block height at which the pure proxy was created. + ext_index: The extrinsic index at which the pure proxy was created. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + The `kill_pure` call must be executed through the pure proxy account itself, with the spawner acting as an + "Any" proxy. This method automatically handles this by executing the call via `proxy()`. The spawner must + have an "Any" proxy relationship with the pure proxy for this to work. + """ + return await kill_pure_proxy_extrinsic( + subtensor=self, + wallet=wallet, + pure_proxy_ss58=pure_proxy_ss58, + spawner=spawner, + proxy_type=proxy_type, + index=index, + height=height, + ext_index=ext_index, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + async def modify_liquidity( self, wallet: "Wallet", @@ -5977,6 +6189,147 @@ async def move_stake( wait_for_finalization=wait_for_finalization, ) + async def poke_deposit( + self, + wallet: "Wallet", + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> ExtrinsicResponse: + """ + Adjusts deposits made for proxies and announcements based on current values. + + This method recalculates and updates the locked deposit amounts for both proxy relationships and announcements + for the signing account. It can be used to potentially lower the locked amount if the deposit requirements have + changed (e.g., due to runtime upgrades or changes in the number of proxies/announcements). + + Parameters: + wallet: Bittensor wallet object (the account whose deposits will be adjusted). + period: The number of blocks during which the transaction will remain valid after it's submitted. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + This method automatically adjusts deposits for both proxy relationships and announcements. No parameters are + needed as it operates on the account's current state. + """ + return await poke_deposit_extrinsic( + subtensor=self, + wallet=wallet, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + async def proxy( + self, + wallet: "Wallet", + real_account_ss58: str, + force_proxy_type: Optional[Union[str, "ProxyType"]], + call: "GenericCall", + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> ExtrinsicResponse: + """ + Executes a call on behalf of the real account through a proxy. + + This method allows a proxy account (delegate) to execute a call on behalf of the real account (delegator). The + call is subject to the permissions defined by the proxy type and must respect the delay period if one was set + when the proxy was added. + + Parameters: + wallet: Bittensor wallet object (should be the proxy account wallet). + real_account_ss58: The SS58 address of the real account on whose behalf the call is being made. + force_proxy_type: The type of proxy to use for the call. If None, any proxy type can be used. Otherwise, must + match one of the allowed proxy types. Can be a string or ProxyType enum value. + call: The inner call to be executed on behalf of the real account. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + The call must be permitted by the proxy type. For example, a "NonTransfer" proxy cannot execute transfer + calls. The delay period must also have passed since the proxy was added. + """ + return await proxy_extrinsic( + subtensor=self, + wallet=wallet, + real_account_ss58=real_account_ss58, + force_proxy_type=force_proxy_type, + call=call, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + async def proxy_announced( + self, + wallet: "Wallet", + delegate_ss58: str, + real_account_ss58: str, + force_proxy_type: Optional[Union[str, "ProxyType"]], + call: "GenericCall", + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> ExtrinsicResponse: + """ + Executes an announced call on behalf of the real account through a proxy. + + This method executes a call that was previously announced via `announce_proxy()`. The call must match the + call_hash that was announced, and the delay period must have passed since the announcement was made. The real + account has the opportunity to review and reject the announcement before execution. + + Parameters: + wallet: Bittensor wallet object (should be the proxy account wallet that made the announcement). + delegate_ss58: The SS58 address of the delegate proxy account that made the announcement. + real_account_ss58: The SS58 address of the real account on whose behalf the call will be made. + force_proxy_type: The type of proxy to use for the call. If None, any proxy type can be used. Otherwise, must + match one of the allowed proxy types. Can be a string or ProxyType enum value. + call: The inner call to be executed on behalf of the real account (must match the announced call_hash). + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + The call_hash of the provided call must match the call_hash that was announced. The announcement must not + have been rejected by the real account, and the delay period must have passed. + """ + return await proxy_announced_extrinsic( + subtensor=self, + wallet=wallet, + delegate_ss58=delegate_ss58, + real_account_ss58=real_account_ss58, + force_proxy_type=force_proxy_type, + call=call, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + async def refund_crowdloan( self, wallet: "Wallet", @@ -6021,6 +6374,51 @@ async def refund_crowdloan( wait_for_finalization=wait_for_finalization, ) + async def reject_proxy_announcement( + self, + wallet: "Wallet", + delegate_ss58: str, + call_hash: str, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> ExtrinsicResponse: + """ + Rejects an announcement made by a proxy delegate. + + This method allows the real account to reject an announcement made by a proxy delegate, preventing the announced + call from being executed. Once rejected, the announcement cannot be executed and the announcement deposit is + returned to the delegate. + + Parameters: + wallet: Bittensor wallet object (should be the real account wallet). + delegate_ss58: The SS58 address of the delegate proxy account whose announcement is being rejected. + call_hash: The hash of the call that was announced and is now being rejected. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + Once rejected, the announcement cannot be executed. The delegate's announcement deposit is returned. + """ + return await reject_announcement_extrinsic( + subtensor=self, + wallet=wallet, + delegate_ss58=delegate_ss58, + call_hash=call_hash, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + async def register( self: "AsyncSubtensor", wallet: "Wallet", @@ -6119,6 +6517,52 @@ async def register_subnet( wait_for_finalization=wait_for_finalization, ) + async def remove_proxy_announcement( + self, + wallet: "Wallet", + real_account_ss58: str, + call_hash: str, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> ExtrinsicResponse: + """ + Removes an announcement made by a proxy account. + + This method allows the proxy account to remove its own announcement before it is executed or rejected. This + frees up the announcement deposit and prevents the call from being executed. Only the proxy account that made + the announcement can remove it. + + Parameters: + wallet: Bittensor wallet object (should be the proxy account wallet that made the announcement). + real_account_ss58: The SS58 address of the real account on whose behalf the call was announced. + call_hash: The hash of the call that was announced and is now being removed. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + Only the proxy account that made the announcement can remove it. The real account can reject it via + `reject_proxy_announcement()`, but cannot remove it directly. + """ + return await remove_announcement_extrinsic( + subtensor=self, + wallet=wallet, + real_account_ss58=real_account_ss58, + call_hash=call_hash, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + async def remove_liquidity( self, wallet: "Wallet", @@ -6164,6 +6608,96 @@ async def remove_liquidity( wait_for_finalization=wait_for_finalization, ) + async def remove_proxies( + self, + wallet: "Wallet", + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> ExtrinsicResponse: + """ + Removes all proxy relationships for the account in a single transaction. + + This method removes all proxy relationships for the signing account in a single call, which is more efficient + than removing them one by one using `remove_proxy()`. The deposit for all proxies will be returned to the + account. + + Parameters: + wallet: Bittensor wallet object. The account whose proxies will be removed (the delegator). All proxy + relationships where wallet.coldkey.ss58_address is the real account will be removed. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + This removes all proxy relationships for the account, regardless of proxy type or delegate. Use + `remove_proxy()` if you need to remove specific proxy relationships selectively. + """ + return await remove_proxies_extrinsic( + subtensor=self, + wallet=wallet, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + async def remove_proxy( + self, + wallet: "Wallet", + delegate_ss58: str, + proxy_type: Union[str, "ProxyType"], + delay: int, + period: Optional[int] = DEFAULT_PERIOD, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> ExtrinsicResponse: + """ + Removes a specific proxy relationship. + + This method removes a single proxy relationship between the real account and a delegate. The parameters must + exactly match those used when the proxy was added via `add_proxy()`. The deposit for this proxy will be returned + to the account. + + Parameters: + wallet: Bittensor wallet object. + delegate_ss58: The SS58 address of the delegate proxy account to remove. + proxy_type: The type of proxy permissions to remove. Can be a string or ProxyType enum value. + delay: The number of blocks before the proxy removal takes effect. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + ExtrinsicResponse: The result object of the extrinsic execution. + + Note: + The delegate_ss58, proxy_type, and delay parameters must exactly match those used when the proxy was added. + Use `get_proxies_for_real_account()` to retrieve the exact parameters for existing proxies. + """ + return await remove_proxy_extrinsic( + subtensor=self, + wallet=wallet, + delegate_ss58=delegate_ss58, + proxy_type=proxy_type, + delay=delay, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + async def reveal_weights( self, wallet: "Wallet", From e0cb23bd7d3451a44009f9387a3af0975189a259 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 10 Nov 2025 12:09:42 -0800 Subject: [PATCH 38/62] oops async properties --- bittensor/core/extrinsics/asyncex/proxy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor/core/extrinsics/asyncex/proxy.py b/bittensor/core/extrinsics/asyncex/proxy.py index 37b1b5d617..85c96e4b46 100644 --- a/bittensor/core/extrinsics/asyncex/proxy.py +++ b/bittensor/core/extrinsics/asyncex/proxy.py @@ -274,7 +274,7 @@ async def create_pure_proxy_extrinsic( logging.debug("[green]Pure proxy created successfully.[/green]") # Extract pure proxy address from PureCreated triggered event - for event in response.extrinsic_receipt.triggered_events: + for event in await response.extrinsic_receipt.triggered_events: if event.get("event_id") == "PureCreated": # Event structure: PureProxyCreated { disambiguation_index, proxy_type, pure, who } attributes = event.get("attributes", []) @@ -287,7 +287,7 @@ async def create_pure_proxy_extrinsic( "height": await subtensor.substrate.get_block_number( response.extrinsic_receipt.block_hash ), - "ext_index": response.extrinsic_receipt.extrinsic_idx, + "ext_index": await response.extrinsic_receipt.extrinsic_idx, } break else: From 18656c3e6d198d0de935105060b8588af47e662a Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 10 Nov 2025 12:14:31 -0800 Subject: [PATCH 39/62] add async e2e tests --- tests/e2e_tests/test_proxy.py | 809 ++++++++++++++++++++++++++++++++-- 1 file changed, 778 insertions(+), 31 deletions(-) diff --git a/tests/e2e_tests/test_proxy.py b/tests/e2e_tests/test_proxy.py index 70320fd576..f4555edcac 100644 --- a/tests/e2e_tests/test_proxy.py +++ b/tests/e2e_tests/test_proxy.py @@ -1,5 +1,6 @@ from bittensor.core.chain_data.proxy import ProxyType from bittensor.core.extrinsics.pallets import SubtensorModule, Proxy, Balances +import pytest def test_proxy_and_errors(subtensor, alice_wallet, bob_wallet, charlie_wallet): @@ -217,6 +218,224 @@ def test_proxy_and_errors(subtensor, alice_wallet, bob_wallet, charlie_wallet): assert len(proxies) == 3 # Staking + ChildKeys + Registration (alice) +@pytest.mark.asyncio +async def test_proxy_and_errors_async( + async_subtensor, alice_wallet, bob_wallet, charlie_wallet +): + """Tests proxy logic with async implementation. + + Steps: + - Verify that chain has no proxies initially. + - Add proxy with ProxyType.Registration and verify success. + - Attempt to add duplicate proxy and verify error handling. + - Add proxy with ProxyType.Transfer and verify success. + - Verify chain has 2 proxies with correct deposit. + - Verify proxy details match expected values (delegate, type, delay). + - Test get_proxies() returns all proxies in network. + - Test get_proxy_constants() returns valid constants. + - Remove proxy ProxyType.Registration and verify deposit decreases. + - Verify chain has 1 proxy remaining. + - Remove proxy ProxyType.Transfer and verify all proxies removed. + - Attempt to remove non-existent proxy and verify NotFound error. + - Attempt to add proxy with invalid type and verify error. + - Attempt to add self as proxy and verify NoSelfProxy error. + - Test adding proxy with delay = 0 and verify it works. + - Test adding multiple proxy types for same delegate. + - Test adding proxy with different delegate. + """ + real_account_wallet = bob_wallet + delegate_wallet = charlie_wallet + delay = 100 + + # === check that chain has no proxies === + proxies, deposit = await async_subtensor.proxies.get_proxies_for_real_account( + real_account_ss58=real_account_wallet.coldkey.ss58_address + ) + assert not proxies + assert deposit == 0 + + # === add proxy with ProxyType.Registration === + response = await async_subtensor.proxies.add_proxy( + wallet=real_account_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + proxy_type=ProxyType.Registration, + delay=delay, + ) + assert response.success, response.message + + # === add the same proxy returns error === + response = await async_subtensor.proxies.add_proxy( + wallet=real_account_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + proxy_type=ProxyType.Registration, + delay=delay, + ) + assert not response.success + assert "Duplicate" in response.message + assert response.error["name"] == "Duplicate" + assert response.error["docs"] == ["Account is already a proxy."] + + # === add proxy with ProxyType.Transfer === + response = await async_subtensor.proxies.add_proxy( + wallet=real_account_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + proxy_type=ProxyType.Transfer, + delay=delay, + ) + assert response.success, response.message + + # === check that chain has 2 proxy === + proxies, deposit = await async_subtensor.proxies.get_proxies_for_real_account( + real_account_ss58=real_account_wallet.coldkey.ss58_address + ) + assert len(proxies) == 2 + assert deposit > 0 + initial_deposit = deposit + + proxy_registration = next( + (p for p in proxies if p.proxy_type == ProxyType.Registration), None + ) + assert proxy_registration is not None + assert proxy_registration.delegate == delegate_wallet.coldkey.ss58_address + assert proxy_registration.proxy_type == ProxyType.Registration + assert proxy_registration.delay == delay + + proxy_transfer = next( + (p for p in proxies if p.proxy_type == ProxyType.Transfer), None + ) + assert proxy_transfer is not None + assert proxy_transfer.delegate == delegate_wallet.coldkey.ss58_address + assert proxy_transfer.proxy_type == ProxyType.Transfer + assert proxy_transfer.delay == delay + + # === Test get_proxies() - all proxies in network === + all_proxies = await async_subtensor.proxies.get_proxies() + assert isinstance(all_proxies, dict) + assert real_account_wallet.coldkey.ss58_address in all_proxies + assert len(all_proxies[real_account_wallet.coldkey.ss58_address]) == 2 + + # === Test get_proxy_constants() === + constants = await async_subtensor.proxies.get_proxy_constants() + assert constants.MaxProxies is not None + assert constants.MaxPending is not None + assert constants.ProxyDepositBase is not None + assert constants.ProxyDepositFactor is not None + + # === remove proxy ProxyType.Registration === + response = await async_subtensor.proxies.remove_proxy( + wallet=real_account_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + proxy_type=ProxyType.Registration, + delay=delay, + ) + assert response.success, response.message + + # === check that chain has 1 proxies === + proxies, deposit = await async_subtensor.proxies.get_proxies_for_real_account( + real_account_ss58=real_account_wallet.coldkey.ss58_address + ) + assert len(proxies) == 1 + assert deposit > 0 + # Deposit should decrease after removing one proxy + assert deposit < initial_deposit + + # === remove proxy ProxyType.Transfer === + response = await async_subtensor.proxies.remove_proxy( + wallet=real_account_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + proxy_type=ProxyType.Transfer, + delay=delay, + ) + assert response.success, response.message + + proxies, deposit = await async_subtensor.proxies.get_proxies_for_real_account( + real_account_ss58=real_account_wallet.coldkey.ss58_address + ) + assert not proxies + assert deposit == 0 + + # === remove already deleted or unexisted proxy === + response = await async_subtensor.proxies.remove_proxy( + wallet=real_account_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + proxy_type=ProxyType.Transfer, + delay=delay, + ) + assert not response.success + assert "NotFound" in response.message + assert response.error["name"] == "NotFound" + assert response.error["docs"] == ["Proxy registration not found."] + + # === add proxy with wrong type === + response = await async_subtensor.proxies.add_proxy( + wallet=real_account_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + proxy_type="custom type", + delay=delay, + ) + assert not response.success + assert "Invalid proxy type" in response.message + + # === add proxy to the same account === + response = await async_subtensor.proxies.add_proxy( + wallet=real_account_wallet, + delegate_ss58=real_account_wallet.coldkey.ss58_address, + proxy_type=ProxyType.Registration, + delay=0, + ) + assert not response.success + assert "NoSelfProxy" in response.message + assert response.error["name"] == "NoSelfProxy" + assert response.error["docs"] == ["Cannot add self as proxy."] + + # === Test adding proxy with delay = 0 === + response = await async_subtensor.proxies.add_proxy( + wallet=real_account_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + proxy_type=ProxyType.Staking, + delay=0, + ) + assert response.success, response.message + + # Verify delay = 0 + proxies, _ = await async_subtensor.proxies.get_proxies_for_real_account( + real_account_ss58=real_account_wallet.coldkey.ss58_address + ) + proxy_staking = next( + (p for p in proxies if p.proxy_type == ProxyType.Staking), None + ) + assert proxy_staking is not None + assert proxy_staking.delay == 0 + + # === Test adding multiple proxy types for same delegate === + response = await async_subtensor.proxies.add_proxy( + wallet=real_account_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + proxy_type=ProxyType.ChildKeys, + delay=delay, + ) + assert response.success, response.message + + proxies, _ = await async_subtensor.proxies.get_proxies_for_real_account( + real_account_ss58=real_account_wallet.coldkey.ss58_address + ) + assert len(proxies) == 2 # Staking + ChildKeys + + # === Test adding proxy with different delegate === + response = await async_subtensor.proxies.add_proxy( + wallet=real_account_wallet, + delegate_ss58=alice_wallet.coldkey.ss58_address, + proxy_type=ProxyType.Registration, + delay=delay, + ) + assert response.success, response.message + + proxies, _ = await async_subtensor.proxies.get_proxies_for_real_account( + real_account_ss58=real_account_wallet.coldkey.ss58_address + ) + assert len(proxies) == 3 # Staking + ChildKeys + Registration (alice) + + def test_create_and_announcement_proxy( subtensor, alice_wallet, bob_wallet, charlie_wallet ): @@ -298,16 +517,286 @@ def test_create_and_announcement_proxy( # === Announce first call (register_network) === call_hash_register = "0x" + subnet_register_call.call_hash.hex() - response = subtensor.proxies.announce_proxy( + response = subtensor.proxies.announce_proxy( + wallet=delegate_wallet, + real_account_ss58=real_account_wallet.coldkey.ss58_address, + call_hash=call_hash_register, + ) + assert response.success, response.message + registration_block = subtensor.block + delay + + # === Test get_proxy_announcements() === + announcements = subtensor.proxies.get_proxy_announcements() + assert len(announcements[delegate_wallet.coldkey.ss58_address]) == 1 + + delegate_announcement = announcements[delegate_wallet.coldkey.ss58_address][0] + assert delegate_announcement.call_hash == call_hash_register + assert delegate_announcement.real == real_account_wallet.coldkey.ss58_address + + # === announced call before delay blocks - register subnet === + response = subtensor.proxies.proxy_announced( + wallet=delegate_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + real_account_ss58=real_account_wallet.coldkey.ss58_address, + force_proxy_type=proxy_type, + call=subnet_register_call, + ) + assert not response.success + assert "Unannounced" in response.message + assert response.error["name"] == "Unannounced" + assert response.error["docs"] == [ + "Announcement, if made at all, was made too recently." + ] + + # === delay block need to be awaited after announcement === + subtensor.wait_for_block(registration_block) + + # === proxy call - register subnet === + response = subtensor.proxies.proxy_announced( + wallet=delegate_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + real_account_ss58=real_account_wallet.coldkey.ss58_address, + force_proxy_type=proxy_type, + call=subnet_register_call, + ) + assert response.success, response.message + assert subtensor.subnets.get_total_subnets() == 3 + + # === Verify announcement is consumed (cannot reuse) === + assert not subtensor.proxies.get_proxy_announcements() + + # === check that subnet is not active === + assert not subtensor.subnets.is_subnet_active(netuid=2) + + # === Announce second call (start_call) === + call_hash_activating = "0x" + subnet_activating_call.call_hash.hex() + response = subtensor.proxies.announce_proxy( + wallet=delegate_wallet, + real_account_ss58=real_account_wallet.coldkey.ss58_address, + call_hash=call_hash_activating, + ) + assert response.success, response.message + + # === Test reject_announcement (real account rejects) === + # Create another announcement to test rejection + test_call = SubtensorModule(subtensor).register_network( + hotkey=alice_wallet.hotkey.ss58_address + ) + test_call_hash = "0x" + test_call.call_hash.hex() + + response = subtensor.proxies.announce_proxy( + wallet=delegate_wallet, + real_account_ss58=real_account_wallet.coldkey.ss58_address, + call_hash=test_call_hash, + ) + assert response.success, response.message + + # Real account rejects the announcement + response = subtensor.proxies.reject_proxy_announcement( + wallet=real_account_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + call_hash=test_call_hash, + ) + assert response.success, response.message + + # Verify announcement was removed + announcements = subtensor.proxies.get_proxy_announcement( + delegate_account_ss58=delegate_wallet.coldkey.ss58_address + ) + # Should only have start_call announcement, test_call should be rejected + assert len(announcements) == 1 + assert announcements[0].call_hash == call_hash_activating + + # === Test remove_announcement (proxy removes its own announcement) === + # Create another announcement + test_call2 = SubtensorModule(subtensor).register_network( + hotkey=alice_wallet.hotkey.ss58_address + ) + test_call_hash2 = "0x" + test_call2.call_hash.hex() + + response = subtensor.proxies.announce_proxy( + wallet=delegate_wallet, + real_account_ss58=real_account_wallet.coldkey.ss58_address, + call_hash=test_call_hash2, + ) + assert response.success, response.message + + # Proxy removes its own announcement + response = subtensor.proxies.remove_proxy_announcement( + wallet=delegate_wallet, + real_account_ss58=real_account_wallet.coldkey.ss58_address, + call_hash=test_call_hash2, + ) + assert response.success, response.message + + # Verify announcement was removed + announcements = subtensor.proxies.get_proxy_announcement( + delegate_account_ss58=delegate_wallet.coldkey.ss58_address + ) + assert len(announcements) == 1 + assert announcements[0].call_hash == call_hash_activating + + # === delay block need to be awaited after announcement === + activation_block = subtensor.block + delay + subtensor.wait_for_block(activation_block) + + # === proxy call - activate subnet === + response = subtensor.proxies.proxy_announced( + wallet=delegate_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + real_account_ss58=real_account_wallet.coldkey.ss58_address, + force_proxy_type=proxy_type, + call=subnet_activating_call, + ) + assert response.success, response.message + assert subtensor.subnets.is_subnet_active(netuid=2) + + # === Test proxy_call with delay = 0 (can be used immediately) === + # Add proxy with delay = 0 + response = subtensor.proxies.add_proxy( + wallet=real_account_wallet, + delegate_ss58=alice_wallet.coldkey.ss58_address, + proxy_type=ProxyType.Any, + delay=0, + ) + assert response.success, response.message + + # With delay = 0, can use proxy_call directly without announcement + test_call3 = SubtensorModule(subtensor).register_network( + hotkey=alice_wallet.hotkey.ss58_address + ) + response = subtensor.proxies.proxy( + wallet=alice_wallet, + real_account_ss58=real_account_wallet.coldkey.ss58_address, + force_proxy_type=ProxyType.Any, + call=test_call3, + ) + assert response.success, response.message + + # === Test proxy_announced with wrong call_hash === + # Create announcement + correct_call = SubtensorModule(subtensor).register_network( + hotkey=alice_wallet.hotkey.ss58_address + ) + correct_call_hash = "0x" + correct_call.call_hash.hex() + + response = subtensor.proxies.announce_proxy( + wallet=alice_wallet, + real_account_ss58=real_account_wallet.coldkey.ss58_address, + call_hash=correct_call_hash, + ) + assert response.success, response.message + + # Wait for delay + subtensor.wait_for_block( + subtensor.block + 1 + ) # delay = 0, so can execute immediately + + # Try to execute with wrong call (different call_hash) + wrong_call = SubtensorModule(subtensor).start_call(netuid=3) + response = subtensor.proxies.proxy_announced( + wallet=alice_wallet, + delegate_ss58=alice_wallet.coldkey.ss58_address, + real_account_ss58=real_account_wallet.coldkey.ss58_address, + force_proxy_type=ProxyType.Any, + call=wrong_call, # Wrong call_hash + ) + # Should fail because call_hash doesn't match + assert not response.success + + +@pytest.mark.asyncio +async def test_create_and_announcement_proxy_async( + async_subtensor, alice_wallet, bob_wallet, charlie_wallet +): + """Tests proxy logic with announcement mechanism for delay > 0 with async implemtntation. + + Steps: + - Add proxy with ProxyType.Any and delay > 0. + - Verify premature proxy call returns Unannounced error. + - Verify premature proxy_announced call without announcement returns error. + - Announce first call (register_network) and verify success. + - Test get_proxy_announcements() returns correct announcements. + - Attempt to execute announced call before delay blocks and verify error. + - Wait for delay blocks to pass. + - Execute proxy_announced call and verify subnet registration success. + - Verify announcement is consumed after execution. + - Verify subnet is not active after registration. + - Announce second call (start_call) for subnet activation. + - Test reject_announcement (real account rejects announcement). + - Verify rejected announcement is removed. + - Test remove_announcement (proxy removes its own announcement). + - Verify removed announcement is no longer present. + - Wait for delay blocks after second announcement. + - Execute proxy_announced call to activate subnet and verify success. + - Test proxy_call with delay = 0 (can be used immediately). + - Test proxy_announced with wrong call_hash and verify error. + """ + # === add proxy again === + real_account_wallet = bob_wallet + delegate_wallet = charlie_wallet + + proxy_type = ProxyType.Any + delay = 30 # cant execute proxy 30 blocks after announcement (not after creation) + + response = await async_subtensor.proxies.add_proxy( + wallet=real_account_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + proxy_type=proxy_type, + delay=delay, + ) + assert response.success, response.message + + # check amount of subnets + assert await async_subtensor.subnets.get_total_subnets() == 2 + + subnet_register_call = await SubtensorModule(async_subtensor).register_network( + hotkey=delegate_wallet.hotkey.ss58_address + ) + subnet_activating_call = await SubtensorModule(async_subtensor).start_call(netuid=2) + + # === premature proxy call === + # if delay > 0 .proxy always returns Unannounced error + response = await async_subtensor.proxies.proxy( + wallet=delegate_wallet, + real_account_ss58=real_account_wallet.coldkey.ss58_address, + force_proxy_type=proxy_type, + call=subnet_activating_call, + ) + assert not response.success + assert "Unannounced" in response.message + assert response.error["name"] == "Unannounced" + assert response.error["docs"] == [ + "Announcement, if made at all, was made too recently." + ] + + # === premature proxy_announced call without announcement === + response = await async_subtensor.proxies.proxy_announced( + wallet=delegate_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + real_account_ss58=real_account_wallet.coldkey.ss58_address, + force_proxy_type=proxy_type, + call=subnet_register_call, + ) + assert not response.success + assert "Unannounced" in response.message + assert response.error["name"] == "Unannounced" + assert response.error["docs"] == [ + "Announcement, if made at all, was made too recently." + ] + + # === Announce first call (register_network) === + call_hash_register = "0x" + subnet_register_call.call_hash.hex() + response = await async_subtensor.proxies.announce_proxy( wallet=delegate_wallet, real_account_ss58=real_account_wallet.coldkey.ss58_address, call_hash=call_hash_register, ) assert response.success, response.message - registration_block = subtensor.block + delay + registration_block = await async_subtensor.block + delay # === Test get_proxy_announcements() === - announcements = subtensor.proxies.get_proxy_announcements() + announcements = await async_subtensor.proxies.get_proxy_announcements() assert len(announcements[delegate_wallet.coldkey.ss58_address]) == 1 delegate_announcement = announcements[delegate_wallet.coldkey.ss58_address][0] @@ -315,7 +804,7 @@ def test_create_and_announcement_proxy( assert delegate_announcement.real == real_account_wallet.coldkey.ss58_address # === announced call before delay blocks - register subnet === - response = subtensor.proxies.proxy_announced( + response = await async_subtensor.proxies.proxy_announced( wallet=delegate_wallet, delegate_ss58=delegate_wallet.coldkey.ss58_address, real_account_ss58=real_account_wallet.coldkey.ss58_address, @@ -330,10 +819,10 @@ def test_create_and_announcement_proxy( ] # === delay block need to be awaited after announcement === - subtensor.wait_for_block(registration_block) + await async_subtensor.wait_for_block(registration_block) # === proxy call - register subnet === - response = subtensor.proxies.proxy_announced( + response = await async_subtensor.proxies.proxy_announced( wallet=delegate_wallet, delegate_ss58=delegate_wallet.coldkey.ss58_address, real_account_ss58=real_account_wallet.coldkey.ss58_address, @@ -341,17 +830,17 @@ def test_create_and_announcement_proxy( call=subnet_register_call, ) assert response.success, response.message - assert subtensor.subnets.get_total_subnets() == 3 + assert await async_subtensor.subnets.get_total_subnets() == 3 # === Verify announcement is consumed (cannot reuse) === - assert not subtensor.proxies.get_proxy_announcements() + assert not await async_subtensor.proxies.get_proxy_announcements() # === check that subnet is not active === - assert not subtensor.subnets.is_subnet_active(netuid=2) + assert not await async_subtensor.subnets.is_subnet_active(netuid=2) # === Announce second call (start_call) === call_hash_activating = "0x" + subnet_activating_call.call_hash.hex() - response = subtensor.proxies.announce_proxy( + response = await async_subtensor.proxies.announce_proxy( wallet=delegate_wallet, real_account_ss58=real_account_wallet.coldkey.ss58_address, call_hash=call_hash_activating, @@ -360,12 +849,12 @@ def test_create_and_announcement_proxy( # === Test reject_announcement (real account rejects) === # Create another announcement to test rejection - test_call = SubtensorModule(subtensor).register_network( + test_call = await SubtensorModule(async_subtensor).register_network( hotkey=alice_wallet.hotkey.ss58_address ) test_call_hash = "0x" + test_call.call_hash.hex() - response = subtensor.proxies.announce_proxy( + response = await async_subtensor.proxies.announce_proxy( wallet=delegate_wallet, real_account_ss58=real_account_wallet.coldkey.ss58_address, call_hash=test_call_hash, @@ -373,7 +862,7 @@ def test_create_and_announcement_proxy( assert response.success, response.message # Real account rejects the announcement - response = subtensor.proxies.reject_proxy_announcement( + response = await async_subtensor.proxies.reject_proxy_announcement( wallet=real_account_wallet, delegate_ss58=delegate_wallet.coldkey.ss58_address, call_hash=test_call_hash, @@ -381,7 +870,7 @@ def test_create_and_announcement_proxy( assert response.success, response.message # Verify announcement was removed - announcements = subtensor.proxies.get_proxy_announcement( + announcements = await async_subtensor.proxies.get_proxy_announcement( delegate_account_ss58=delegate_wallet.coldkey.ss58_address ) # Should only have start_call announcement, test_call should be rejected @@ -390,12 +879,12 @@ def test_create_and_announcement_proxy( # === Test remove_announcement (proxy removes its own announcement) === # Create another announcement - test_call2 = SubtensorModule(subtensor).register_network( + test_call2 = await SubtensorModule(async_subtensor).register_network( hotkey=alice_wallet.hotkey.ss58_address ) test_call_hash2 = "0x" + test_call2.call_hash.hex() - response = subtensor.proxies.announce_proxy( + response = await async_subtensor.proxies.announce_proxy( wallet=delegate_wallet, real_account_ss58=real_account_wallet.coldkey.ss58_address, call_hash=test_call_hash2, @@ -403,7 +892,7 @@ def test_create_and_announcement_proxy( assert response.success, response.message # Proxy removes its own announcement - response = subtensor.proxies.remove_proxy_announcement( + response = await async_subtensor.proxies.remove_proxy_announcement( wallet=delegate_wallet, real_account_ss58=real_account_wallet.coldkey.ss58_address, call_hash=test_call_hash2, @@ -411,18 +900,18 @@ def test_create_and_announcement_proxy( assert response.success, response.message # Verify announcement was removed - announcements = subtensor.proxies.get_proxy_announcement( + announcements = await async_subtensor.proxies.get_proxy_announcement( delegate_account_ss58=delegate_wallet.coldkey.ss58_address ) assert len(announcements) == 1 assert announcements[0].call_hash == call_hash_activating # === delay block need to be awaited after announcement === - activation_block = subtensor.block + delay - subtensor.wait_for_block(activation_block) + activation_block = await async_subtensor.block + delay + await async_subtensor.wait_for_block(activation_block) # === proxy call - activate subnet === - response = subtensor.proxies.proxy_announced( + response = await async_subtensor.proxies.proxy_announced( wallet=delegate_wallet, delegate_ss58=delegate_wallet.coldkey.ss58_address, real_account_ss58=real_account_wallet.coldkey.ss58_address, @@ -430,11 +919,11 @@ def test_create_and_announcement_proxy( call=subnet_activating_call, ) assert response.success, response.message - assert subtensor.subnets.is_subnet_active(netuid=2) + assert await async_subtensor.subnets.is_subnet_active(netuid=2) # === Test proxy_call with delay = 0 (can be used immediately) === # Add proxy with delay = 0 - response = subtensor.proxies.add_proxy( + response = await async_subtensor.proxies.add_proxy( wallet=real_account_wallet, delegate_ss58=alice_wallet.coldkey.ss58_address, proxy_type=ProxyType.Any, @@ -443,10 +932,10 @@ def test_create_and_announcement_proxy( assert response.success, response.message # With delay = 0, can use proxy_call directly without announcement - test_call3 = SubtensorModule(subtensor).register_network( + test_call3 = await SubtensorModule(async_subtensor).register_network( hotkey=alice_wallet.hotkey.ss58_address ) - response = subtensor.proxies.proxy( + response = await async_subtensor.proxies.proxy( wallet=alice_wallet, real_account_ss58=real_account_wallet.coldkey.ss58_address, force_proxy_type=ProxyType.Any, @@ -456,12 +945,12 @@ def test_create_and_announcement_proxy( # === Test proxy_announced with wrong call_hash === # Create announcement - correct_call = SubtensorModule(subtensor).register_network( + correct_call = await SubtensorModule(async_subtensor).register_network( hotkey=alice_wallet.hotkey.ss58_address ) correct_call_hash = "0x" + correct_call.call_hash.hex() - response = subtensor.proxies.announce_proxy( + response = await async_subtensor.proxies.announce_proxy( wallet=alice_wallet, real_account_ss58=real_account_wallet.coldkey.ss58_address, call_hash=correct_call_hash, @@ -469,13 +958,13 @@ def test_create_and_announcement_proxy( assert response.success, response.message # Wait for delay - subtensor.wait_for_block( - subtensor.block + 1 + await async_subtensor.wait_for_block( + await async_subtensor.block + 1 ) # delay = 0, so can execute immediately # Try to execute with wrong call (different call_hash) - wrong_call = SubtensorModule(subtensor).start_call(netuid=3) - response = subtensor.proxies.proxy_announced( + wrong_call = await SubtensorModule(async_subtensor).start_call(netuid=3) + response = await async_subtensor.proxies.proxy_announced( wallet=alice_wallet, delegate_ss58=alice_wallet.coldkey.ss58_address, real_account_ss58=real_account_wallet.coldkey.ss58_address, @@ -612,6 +1101,136 @@ def test_create_and_kill_pure_proxy(subtensor, alice_wallet, bob_wallet): ] +@pytest.mark.asyncio +async def test_create_and_kill_pure_proxy_async( + async_subtensor, alice_wallet, bob_wallet +): + """Tests create_pure_proxy and kill_pure_proxy extrinsics with async implementation. + + This test verifies the complete lifecycle of a pure proxy account: + - Creation of a pure proxy with specific parameters + - Verification that the pure proxy can execute calls through the spawner + - Proper termination of the pure proxy + - Confirmation that the killed pure proxy can no longer be used + + Steps: + - Create pure proxy with ProxyType.Any, delay=0, and index=0. + - Extract pure proxy address, spawner, and creation metadata from response.data. + - Verify all required data is present and correctly formatted. + - Fund the pure proxy account so it can execute transfers. + - Execute a transfer through the pure proxy to verify it works correctly. + The spawner acts as an "Any" proxy for the pure proxy account. + - Kill the pure proxy using kill_pure_proxy() method, which automatically + executes the kill_pure call through proxy() (spawner acts as Any proxy + for pure proxy, with pure proxy as the origin). + - Verify pure proxy is killed by attempting to use it and confirming + it returns a NotProxy error. + """ + spawner_wallet = bob_wallet + proxy_type = ProxyType.Any + delay = 0 + index = 0 + + # === Create pure proxy === + response = await async_subtensor.proxies.create_pure_proxy( + wallet=spawner_wallet, + proxy_type=proxy_type, + delay=delay, + index=index, + raise_error=True, + ) + assert response.success, response.message + + # === Extract pure proxy data from response.data === + pure_account = response.data.get("pure_account") + spawner = response.data.get("spawner") + proxy_type_from_response = response.data.get("proxy_type") + index_from_response = response.data.get("index") + height = response.data.get("height") + ext_index = response.data.get("ext_index") + + # === Verify spawner matches === + assert spawner == spawner_wallet.coldkey.ss58_address + + # === Verify all required data is present === + assert pure_account, "Pure account should be present." + assert spawner, "Spawner should be present." + assert proxy_type_from_response, "Proxy type should be present." + assert isinstance(index_from_response, int) + assert isinstance(height, int) and height > 0 + assert isinstance(ext_index, int) and ext_index >= 0 + + # === Fund the pure proxy account so it can execute transfers === + from bittensor.utils.balance import Balance + + fund_amount = Balance.from_tao(1.0) # Fund with 1 TAO + response = await async_subtensor.wallets.transfer( + wallet=spawner_wallet, + destination_ss58=pure_account, + amount=fund_amount, + ) + assert response.success, f"Failed to fund pure proxy account: {response.message}." + + # === Test that pure proxy works by executing a transfer through it === + # The spawner acts as an "Any" proxy for the pure proxy account. + # The pure proxy account is the origin (real account), and the spawner signs the transaction. + transfer_amount = Balance.from_tao(0.1) # Transfer 0.1 TAO + transfer_call = await Balances(async_subtensor).transfer_keep_alive( + dest=alice_wallet.coldkey.ss58_address, + value=transfer_amount.rao, + ) + + response = await async_subtensor.proxies.proxy( + wallet=spawner_wallet, # Spawner signs the transaction + real_account_ss58=pure_account, # Pure proxy account is the origin (real) + force_proxy_type=ProxyType.Any, # Spawner acts as Any proxy for pure proxy + call=transfer_call, + ) + assert response.success, ( + f"Pure proxy should be able to execute transfers, got: {response.message}." + ) + + # === Kill pure proxy using kill_pure_proxy() method === + # The kill_pure_proxy() method automatically executes the kill_pure call through proxy(): + # - The spawner signs the transaction (wallet parameter) + # - The pure proxy account is the origin (real_account_ss58 parameter) + # - The spawner acts as an "Any" proxy for the pure proxy (force_proxy_type=Any) + # This is required because pure proxies are keyless accounts and cannot sign transactions directly. + response = await async_subtensor.proxies.kill_pure_proxy( + wallet=spawner_wallet, + pure_proxy_ss58=pure_account, + spawner=spawner, + proxy_type=proxy_type_from_response, + index=index_from_response, + height=height, + ext_index=ext_index, + ) + assert response.success, response.message + + # === Verify pure proxy is killed by attempting to use it === + # Create a simple transfer call to test that proxy fails + simple_call = await Balances(async_subtensor).transfer_keep_alive( + dest=alice_wallet.coldkey.ss58_address, + value=500, # Small amount, just to test + ) + + # === Attempt to execute call through killed pure proxy - should fail === + response = await async_subtensor.proxies.proxy( + wallet=spawner_wallet, + real_account_ss58=pure_account, # Killed pure proxy account + force_proxy_type=ProxyType.Any, + call=simple_call, + ) + + # === Should fail because pure proxy no longer exists === + assert not response.success, "Call through killed pure proxy should fail." + assert "NotProxy" in response.message + assert response.error["name"] == "NotProxy" + assert response.error["docs"] == [ + "Sender is not a proxy of the account to be proxied." + ] + + def test_remove_proxies(subtensor, alice_wallet, bob_wallet, charlie_wallet): """Tests remove_proxies extrinsic. @@ -672,6 +1291,69 @@ def test_remove_proxies(subtensor, alice_wallet, bob_wallet, charlie_wallet): assert deposit == 0 +@pytest.mark.asyncio +async def test_remove_proxies_async( + async_subtensor, alice_wallet, bob_wallet, charlie_wallet +): + """Tests remove_proxies extrinsic with async implementation. + + Steps: + - Add multiple proxies with different types and delegates + - Verify all proxies exist and deposit is correct + - Call remove_proxies to remove all at once + - Verify all proxies are removed + - Verify deposit is returned (should be 0 or empty) + """ + real_account_wallet = bob_wallet + delegate1 = charlie_wallet + delegate2 = alice_wallet + + # === Add multiple proxies === + response = await async_subtensor.proxies.add_proxy( + wallet=real_account_wallet, + delegate_ss58=delegate1.coldkey.ss58_address, + proxy_type=ProxyType.Registration, + delay=0, + ) + assert response.success + + response = await async_subtensor.proxies.add_proxy( + wallet=real_account_wallet, + delegate_ss58=delegate1.coldkey.ss58_address, + proxy_type=ProxyType.Transfer, + delay=0, + ) + assert response.success + + response = await async_subtensor.proxies.add_proxy( + wallet=real_account_wallet, + delegate_ss58=delegate2.coldkey.ss58_address, + proxy_type=ProxyType.Staking, + delay=0, + ) + assert response.success + + # === Verify all proxies exist === + proxies, deposit = await async_subtensor.proxies.get_proxies_for_real_account( + real_account_ss58=real_account_wallet.coldkey.ss58_address + ) + assert len(proxies) == 3 + assert deposit > 0 + + # === Remove all proxies === + response = await async_subtensor.proxies.remove_proxies( + wallet=real_account_wallet, + ) + assert response.success, response.message + + # === Verify all proxies removed === + proxies, deposit = await async_subtensor.proxies.get_proxies_for_real_account( + real_account_ss58=real_account_wallet.coldkey.ss58_address + ) + assert not proxies + assert deposit == 0 + + def test_poke_deposit(subtensor, alice_wallet, bob_wallet, charlie_wallet): """Tests poke_deposit extrinsic. @@ -732,3 +1414,68 @@ def test_poke_deposit(subtensor, alice_wallet, bob_wallet, charlie_wallet): ) # Deposit should match or be adjusted based on current requirements assert final_deposit >= 0 + + +@pytest.mark.asyncio +async def test_poke_deposit_async( + async_subtensor, alice_wallet, bob_wallet, charlie_wallet +): + """Tests poke_deposit extrinsic with async implementation. + + Steps: + - Add multiple proxies and announcements + - Verify initial deposit amount + - Call poke_deposit to recalculate deposits + - Verify deposit may change (if requirements changed) + - Verify transaction fee is waived if deposit changed + """ + real_account_wallet = bob_wallet + delegate_wallet = charlie_wallet + + # Add proxies + response = await async_subtensor.proxies.add_proxy( + wallet=real_account_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + proxy_type=ProxyType.Registration, + delay=0, + ) + assert response.success + + response = await async_subtensor.proxies.add_proxy( + wallet=real_account_wallet, + delegate_ss58=delegate_wallet.coldkey.ss58_address, + proxy_type=ProxyType.Transfer, + delay=0, + ) + assert response.success + + # Get initial deposit + _, initial_deposit = await async_subtensor.proxies.get_proxies_for_real_account( + real_account_ss58=real_account_wallet.coldkey.ss58_address + ) + + # Create an announcement + test_call = await SubtensorModule(async_subtensor).register_network( + hotkey=alice_wallet.hotkey.ss58_address + ) + call_hash = "0x" + test_call.call_hash.hex() + + response = await async_subtensor.proxies.announce_proxy( + wallet=delegate_wallet, + real_account_ss58=real_account_wallet.coldkey.ss58_address, + call_hash=call_hash, + ) + assert response.success + + # Call poke_deposit + response = await async_subtensor.proxies.poke_deposit( + wallet=real_account_wallet, + ) + assert response.success, response.message + + # Verify deposit is still correct (or adjusted) + _, final_deposit = await async_subtensor.proxies.get_proxies_for_real_account( + real_account_ss58=real_account_wallet.coldkey.ss58_address + ) + # Deposit should match or be adjusted based on current requirements + assert final_deposit >= 0 From 3dcb73beafb42c5f7a4c85cf59f1ca4b1a1b96fc Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 10 Nov 2025 13:14:42 -0800 Subject: [PATCH 40/62] make it easy --- bittensor/core/async_subtensor.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index cb7a870345..4c8e963da5 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -3088,10 +3088,9 @@ async def get_proxies( ) proxies = {} - if query_map.records: - async for record in query_map: - real_account, proxy_list = ProxyInfo.from_query_map_record(record) - proxies[real_account] = proxy_list + async for record in query_map: + real_account, proxy_list = ProxyInfo.from_query_map_record(record) + proxies[real_account] = proxy_list return proxies async def get_proxies_for_real_account( @@ -3203,12 +3202,9 @@ async def get_proxy_announcements( reuse_block_hash=reuse_block, ) announcements = {} - if query_map.records: - async for record in query_map: - delegate, proxy_list = ProxyAnnouncementInfo.from_query_map_record( - record - ) - announcements[delegate] = proxy_list + async for record in query_map: + delegate, proxy_list = ProxyAnnouncementInfo.from_query_map_record(record) + announcements[delegate] = proxy_list return announcements async def get_proxy_constants( From 72d06bbe5f7a675b55eec3102760c21b6a1f10b8 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 10 Nov 2025 13:15:03 -0800 Subject: [PATCH 41/62] improve sync tests --- tests/unit_tests/test_subtensor.py | 406 ++--------------------------- 1 file changed, 28 insertions(+), 378 deletions(-) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 1faeb06df5..47df324f5f 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -5034,7 +5034,7 @@ def test_get_ema_tao_inflow(subtensor, mocker): assert result == (fake_block_updated, Balance.from_rao(1000000)) -def test_get_proxies_success(subtensor, mocker): +def test_get_proxies(subtensor, mocker): """Test get_proxies returns correct data when proxy information is found.""" # Prep block = 123 @@ -5092,162 +5092,19 @@ def test_get_proxies_success(subtensor, mocker): assert fake_real_account2 in result -def test_get_proxies_no_data(subtensor, mocker): - """Test get_proxies returns empty dict when no proxy information is found.""" - # Prep - block = 123 - mocked_determine_block_hash = mocker.patch.object( - subtensor, "determine_block_hash", return_value="mock_block_hash" - ) - mocked_query_map = mocker.patch.object( - subtensor.substrate, - "query_map", - return_value=[], - ) - - # Call - result = subtensor.get_proxies(block=block) - - # Asserts - mocked_determine_block_hash.assert_called_once_with(block) - mocked_query_map.assert_called_once_with( - module="Proxy", - storage_function="Proxies", - block_hash="mock_block_hash", - ) - assert result == {} - - -def test_get_proxies_no_block(subtensor, mocker): - """Test get_proxies with no block specified.""" - # Prep - mocked_determine_block_hash = mocker.patch.object( - subtensor, "determine_block_hash", return_value="mock_block_hash" - ) - mocked_query_map = mocker.patch.object( - subtensor.substrate, - "query_map", - return_value=[], - ) - - # Call - result = subtensor.get_proxies() - - # Asserts - mocked_determine_block_hash.assert_called_once_with(None) - mocked_query_map.assert_called_once_with( - module="Proxy", - storage_function="Proxies", - block_hash="mock_block_hash", - ) - assert result == {} - - -def test_get_proxies_for_real_account_success(subtensor, mocker): +def test_get_proxies_for_real_account(subtensor, mocker): """Test get_proxies_for_real_account returns correct data when proxy information is found.""" # Prep - fake_real_account_ss58 = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" - block = 123 - fake_proxy_data = [ - { - "delegate": {"Id": b"\x00" * 32}, - "proxy_type": {"Any": None}, - "delay": 0, - } - ] - fake_balance = 1000000 - fake_query_result = mocker.Mock(value=([fake_proxy_data], fake_balance)) - - mocked_determine_block_hash = mocker.patch.object( - subtensor, "determine_block_hash", return_value="mock_block_hash" - ) - mocked_query = mocker.patch.object( - subtensor.substrate, - "query", - return_value=fake_query_result, - ) - mocked_from_query = mocker.patch.object( - subtensor_module.ProxyInfo, - "from_query", - return_value=([mocker.Mock()], Balance.from_rao(fake_balance)), - ) - - # Call - result = subtensor.get_proxies_for_real_account( - real_account_ss58=fake_real_account_ss58, block=block - ) - - # Asserts - mocked_determine_block_hash.assert_called_once_with(block) - mocked_query.assert_called_once_with( - module="Proxy", - storage_function="Proxies", - params=[fake_real_account_ss58], - block_hash="mock_block_hash", - ) - mocked_from_query.assert_called_once_with(fake_query_result) - assert isinstance(result, tuple) - assert len(result) == 2 - assert isinstance(result[0], list) - assert isinstance(result[1], Balance) - - -def test_get_proxies_for_real_account_no_data(subtensor, mocker): - """Test get_proxies_for_real_account returns empty list and zero balance when no proxy information is found.""" - # Prep - fake_real_account_ss58 = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" - block = 123 - fake_query_result = mocker.Mock(value=([], 0)) - - mocked_determine_block_hash = mocker.patch.object( - subtensor, "determine_block_hash", return_value="mock_block_hash" - ) - mocked_query = mocker.patch.object( - subtensor.substrate, - "query", - return_value=fake_query_result, - ) - mocked_from_query = mocker.patch.object( - subtensor_module.ProxyInfo, - "from_query", - return_value=([], Balance.from_rao(0)), - ) - - # Call - result = subtensor.get_proxies_for_real_account( - real_account_ss58=fake_real_account_ss58, block=block - ) - - # Asserts - mocked_determine_block_hash.assert_called_once_with(block) - mocked_query.assert_called_once_with( - module="Proxy", - storage_function="Proxies", - params=[fake_real_account_ss58], - block_hash="mock_block_hash", - ) - mocked_from_query.assert_called_once_with(fake_query_result) - assert result == ([], Balance.from_rao(0)) - + fake_real_account_ss58 = mocker.Mock(spec=str) -def test_get_proxies_for_real_account_no_block(subtensor, mocker): - """Test get_proxies_for_real_account with no block specified.""" - # Prep - fake_real_account_ss58 = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" - fake_query_result = mocker.Mock(value=([], 0)) - - mocked_determine_block_hash = mocker.patch.object( - subtensor, "determine_block_hash", return_value="mock_block_hash" - ) + mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") mocked_query = mocker.patch.object( subtensor.substrate, "query", - return_value=fake_query_result, ) mocked_from_query = mocker.patch.object( subtensor_module.ProxyInfo, "from_query", - return_value=([], Balance.from_rao(0)), ) # Call @@ -5261,113 +5118,24 @@ def test_get_proxies_for_real_account_no_block(subtensor, mocker): module="Proxy", storage_function="Proxies", params=[fake_real_account_ss58], - block_hash="mock_block_hash", + block_hash=mocked_determine_block_hash.return_value, ) - mocked_from_query.assert_called_once_with(fake_query_result) - assert result == ([], Balance.from_rao(0)) + mocked_from_query.assert_called_once_with(mocked_query.return_value) + assert result == mocked_from_query.return_value -def test_get_proxy_announcement_success(subtensor, mocker): +def test_get_proxy_announcement(subtensor, mocker): """Test get_proxy_announcement returns correct data when announcement information is found.""" # Prep - fake_delegate_account_ss58 = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" - block = 123 - fake_announcement_data = [ - { - "real": {"Id": b"\x00" * 32}, - "call_hash": {"H256": b"\x01" * 32}, - "height": 100, - } - ] - fake_query_result = mocker.Mock(value=[fake_announcement_data]) - - mocked_determine_block_hash = mocker.patch.object( - subtensor, "determine_block_hash", return_value="mock_block_hash" - ) - mocked_query = mocker.patch.object( - subtensor.substrate, - "query", - return_value=fake_query_result, - ) - mocked_from_dict = mocker.patch.object( - subtensor_module.ProxyAnnouncementInfo, - "from_dict", - return_value=[mocker.Mock()], - ) - - # Call - result = subtensor.get_proxy_announcement( - delegate_account_ss58=fake_delegate_account_ss58, block=block - ) - - # Asserts - mocked_determine_block_hash.assert_called_once_with(block) - mocked_query.assert_called_once_with( - module="Proxy", - storage_function="Announcements", - params=[fake_delegate_account_ss58], - block_hash="mock_block_hash", - ) - mocked_from_dict.assert_called_once_with(fake_query_result.value[0]) - assert isinstance(result, list) - - -def test_get_proxy_announcement_no_data(subtensor, mocker): - """Test get_proxy_announcement returns empty list when no announcement information is found.""" - # Prep - fake_delegate_account_ss58 = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" - block = 123 - fake_query_result = mocker.Mock(value=[[]]) - - mocked_determine_block_hash = mocker.patch.object( - subtensor, "determine_block_hash", return_value="mock_block_hash" - ) - mocked_query = mocker.patch.object( - subtensor.substrate, - "query", - return_value=fake_query_result, - ) - mocked_from_dict = mocker.patch.object( - subtensor_module.ProxyAnnouncementInfo, - "from_dict", - return_value=[], - ) - - # Call - result = subtensor.get_proxy_announcement( - delegate_account_ss58=fake_delegate_account_ss58, block=block - ) - - # Asserts - mocked_determine_block_hash.assert_called_once_with(block) - mocked_query.assert_called_once_with( - module="Proxy", - storage_function="Announcements", - params=[fake_delegate_account_ss58], - block_hash="mock_block_hash", - ) - mocked_from_dict.assert_called_once_with(fake_query_result.value[0]) - assert result == [] - - -def test_get_proxy_announcement_no_block(subtensor, mocker): - """Test get_proxy_announcement with no block specified.""" - # Prep - fake_delegate_account_ss58 = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" - fake_query_result = mocker.Mock(value=[[]]) - - mocked_determine_block_hash = mocker.patch.object( - subtensor, "determine_block_hash", return_value="mock_block_hash" - ) + fake_delegate_account_ss58 = mocker.Mock(spec=str) + mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") mocked_query = mocker.patch.object( subtensor.substrate, "query", - return_value=fake_query_result, ) mocked_from_dict = mocker.patch.object( subtensor_module.ProxyAnnouncementInfo, "from_dict", - return_value=[], ) # Call @@ -5381,40 +5149,24 @@ def test_get_proxy_announcement_no_block(subtensor, mocker): module="Proxy", storage_function="Announcements", params=[fake_delegate_account_ss58], - block_hash="mock_block_hash", + block_hash=mocked_determine_block_hash.return_value, ) - mocked_from_dict.assert_called_once_with(fake_query_result.value[0]) - assert result == [] + mocked_from_dict.assert_called_once_with(mocked_query.return_value.value[0]) + assert result == mocked_from_dict.return_value -def test_get_proxy_announcements_success(subtensor, mocker): +def test_get_proxy_announcements(subtensor, mocker): """Test get_proxy_announcements returns correct data when announcement information is found.""" # Prep - block = 123 - fake_delegate1 = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" - fake_delegate2 = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" - fake_announcement_data1 = [ - { - "real": {"Id": b"\x00" * 32}, - "call_hash": {"H256": b"\x01" * 32}, - "height": 100, - } - ] - fake_announcement_data2 = [ - { - "real": {"Id": b"\x02" * 32}, - "call_hash": {"H256": b"\x03" * 32}, - "height": 200, - } - ] - fake_query_map_records = [ - (fake_delegate1.encode(), mocker.Mock(value=[fake_announcement_data1])), - (fake_delegate2.encode(), mocker.Mock(value=[fake_announcement_data2])), - ] - + fake_delegate = mocker.Mock(spec=str) + fake_proxies_list = mocker.Mock(spec=list) mocked_determine_block_hash = mocker.patch.object( subtensor, "determine_block_hash", return_value="mock_block_hash" ) + + fake_record = (fake_delegate, fake_proxies_list) + fake_query_map_records = [fake_record] + mocked_query_map = mocker.patch.object( subtensor.substrate, "query_map", @@ -5423,64 +5175,7 @@ def test_get_proxy_announcements_success(subtensor, mocker): mocked_from_query_map_record = mocker.patch.object( subtensor_module.ProxyAnnouncementInfo, "from_query_map_record", - side_effect=[ - (fake_delegate1, [mocker.Mock()]), - (fake_delegate2, [mocker.Mock()]), - ], - ) - - # Call - result = subtensor.get_proxy_announcements(block=block) - - # Asserts - mocked_determine_block_hash.assert_called_once_with(block) - mocked_query_map.assert_called_once_with( - module="Proxy", - storage_function="Announcements", - block_hash="mock_block_hash", - ) - assert mocked_from_query_map_record.call_count == 2 - assert isinstance(result, dict) - assert fake_delegate1 in result - assert fake_delegate2 in result - - -def test_get_proxy_announcements_no_data(subtensor, mocker): - """Test get_proxy_announcements returns empty dict when no announcement information is found.""" - # Prep - block = 123 - mocked_determine_block_hash = mocker.patch.object( - subtensor, "determine_block_hash", return_value="mock_block_hash" - ) - mocked_query_map = mocker.patch.object( - subtensor.substrate, - "query_map", - return_value=[], - ) - - # Call - result = subtensor.get_proxy_announcements(block=block) - - # Asserts - mocked_determine_block_hash.assert_called_once_with(block) - mocked_query_map.assert_called_once_with( - module="Proxy", - storage_function="Announcements", - block_hash="mock_block_hash", - ) - assert result == {} - - -def test_get_proxy_announcements_no_block(subtensor, mocker): - """Test get_proxy_announcements with no block specified.""" - # Prep - mocked_determine_block_hash = mocker.patch.object( - subtensor, "determine_block_hash", return_value="mock_block_hash" - ) - mocked_query_map = mocker.patch.object( - subtensor.substrate, - "query_map", - return_value=[], + side_effect=fake_query_map_records, ) # Call @@ -5491,15 +5186,15 @@ def test_get_proxy_announcements_no_block(subtensor, mocker): mocked_query_map.assert_called_once_with( module="Proxy", storage_function="Announcements", - block_hash="mock_block_hash", + block_hash=mocked_determine_block_hash.return_value, ) - assert result == {} + mocked_from_query_map_record.assert_called_once_with(fake_record) + assert result == {fake_delegate: fake_proxies_list} -def test_get_proxy_constants_success(subtensor, mocker): +def test_get_proxy_constants(subtensor, mocker): """Test get_proxy_constants returns correct data when constants are found.""" # Prep - block = 123 fake_constants = { "AnnouncementDepositBase": 1000000, "AnnouncementDepositFactor": 500000, @@ -5514,19 +5209,10 @@ def test_get_proxy_constants_success(subtensor, mocker): "query_constant", side_effect=[mocker.Mock(value=value) for value in fake_constants.values()], ) - mocked_from_dict = mocker.patch.object( - subtensor_module.ProxyConstants, - "from_dict", - return_value=mocker.Mock(), - ) - mocked_to_dict = mocker.patch.object( - mocked_from_dict.return_value, - "to_dict", - return_value=fake_constants, - ) + mocked_from_dict = mocker.patch.object(subtensor_module.ProxyConstants, "from_dict") # Call - result = subtensor.get_proxy_constants(block=block) + result = subtensor.get_proxy_constants() # Asserts assert mocked_query_constant.call_count == len(fake_constants) @@ -5537,7 +5223,6 @@ def test_get_proxy_constants_success(subtensor, mocker): def test_get_proxy_constants_as_dict(subtensor, mocker): """Test get_proxy_constants returns dict when as_dict=True.""" # Prep - block = 123 fake_constants = { "AnnouncementDepositBase": 1000000, "AnnouncementDepositFactor": 500000, @@ -5565,7 +5250,7 @@ def test_get_proxy_constants_as_dict(subtensor, mocker): ) # Call - result = subtensor.get_proxy_constants(block=block, as_dict=True) + result = subtensor.get_proxy_constants(as_dict=True) # Asserts assert mocked_query_constant.call_count == len(fake_constants) @@ -5574,41 +5259,6 @@ def test_get_proxy_constants_as_dict(subtensor, mocker): assert result == fake_constants -def test_get_proxy_constants_specific_constants(subtensor, mocker): - """Test get_proxy_constants with specific constants list.""" - # Prep - block = 123 - requested_constants = ["MaxProxies", "MaxPending"] - fake_constants = { - "MaxProxies": 32, - "MaxPending": 32, - } - - mocked_query_constant = mocker.patch.object( - subtensor, - "query_constant", - side_effect=[mocker.Mock(value=value) for value in fake_constants.values()], - ) - mocked_from_dict = mocker.patch.object( - subtensor_module.ProxyConstants, - "from_dict", - return_value=mocker.Mock(), - ) - - # Call - result = subtensor.get_proxy_constants(constants=requested_constants, block=block) - - # Asserts - assert mocked_query_constant.call_count == len(requested_constants) - for const_name in requested_constants: - mocked_query_constant.assert_any_call( - module_name="Proxy", - constant_name=const_name, - block=block, - ) - mocked_from_dict.assert_called_once_with(fake_constants) - - def test_add_proxy(mocker, subtensor): """Tests `add_proxy` extrinsic call method.""" # preps From 34ccfe9b553de7bbd4d192cbbade8af792dd210b Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 10 Nov 2025 13:15:12 -0800 Subject: [PATCH 42/62] add async unit tests --- tests/unit_tests/test_async_subtensor.py | 587 +++++++++++++++++++++++ 1 file changed, 587 insertions(+) diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index a75413efa8..6b90acd684 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -4957,3 +4957,590 @@ async def test_get_ema_tao_inflow(subtensor, mocker): ) mocked_fixed_to_float.assert_called_once_with(fake_tao_bits) assert result == (fake_block_updated, Balance.from_rao(1000000)) + + +@pytest.mark.asyncio +async def test_get_proxies(subtensor, mocker): + """Test get_proxies returns correct data when proxy information is found.""" + # Prep + fake_real_account = mocker.Mock(spec=str) + + mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + + fake_proxy_data = mocker.Mock(spec=dict) + fake_record = ( + fake_real_account, + mocker.Mock(value=([fake_proxy_data], mocker.Mock(spec=Balance))), + ) + fake_result = [fake_record] + fake_query_map_records = mocker.MagicMock(return_value=fake_result) + fake_query_map_records.__aiter__.return_value = iter(fake_result) + + mocked_query_map = mocker.patch.object( + subtensor.substrate, + "query_map", + return_value=fake_query_map_records, + ) + fake_proxy_list = mocker.Mock() + mocked_from_query_map_record = mocker.patch.object( + async_subtensor.ProxyInfo, + "from_query_map_record", + side_effect=[ + (fake_real_account, [fake_proxy_list]), + ], + ) + + # Call + result = await subtensor.get_proxies() + + # Asserts + mocked_determine_block_hash.assert_awaited_once_with(None, None, False) + mocked_query_map.assert_awaited_once_with( + module="Proxy", + storage_function="Proxies", + block_hash=mocked_determine_block_hash.return_value, + reuse_block_hash=False, + ) + mocked_from_query_map_record.assert_called_once_with(fake_record) + assert result == {fake_real_account: [fake_proxy_list]} + + +@pytest.mark.asyncio +async def test_get_proxies_for_real_account(subtensor, mocker): + """Test get_proxies_for_real_account returns correct data when proxy information is found.""" + # Prep + fake_real_account_ss58 = mocker.Mock(spec=str) + + mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + mocked_query = mocker.patch.object( + subtensor.substrate, + "query", + ) + mocked_from_query = mocker.patch.object( + async_subtensor.ProxyInfo, + "from_query", + ) + + # Call + result = await subtensor.get_proxies_for_real_account( + real_account_ss58=fake_real_account_ss58 + ) + + # Asserts + mocked_determine_block_hash.assert_awaited_once_with(None, None, False) + mocked_query.assert_awaited_once_with( + module="Proxy", + storage_function="Proxies", + params=[fake_real_account_ss58], + block_hash=mocked_determine_block_hash.return_value, + reuse_block_hash=False, + ) + mocked_from_query.assert_called_once_with(mocked_query.return_value) + assert result == mocked_from_query.return_value + + +@pytest.mark.asyncio +async def test_get_proxy_announcement(subtensor, mocker): + """Test get_proxy_announcement returns correct data when announcement information is found.""" + # Prep + fake_delegate_account_ss58 = mocker.Mock(spec=str) + mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + mocked_query = mocker.patch.object( + subtensor.substrate, + "query", + ) + mocked_from_dict = mocker.patch.object( + async_subtensor.ProxyAnnouncementInfo, + "from_dict", + ) + + # Call + result = await subtensor.get_proxy_announcement( + delegate_account_ss58=fake_delegate_account_ss58 + ) + + # Asserts + mocked_determine_block_hash.assert_awaited_once_with(None, None, False) + mocked_query.assert_awaited_once_with( + module="Proxy", + storage_function="Announcements", + params=[fake_delegate_account_ss58], + block_hash=mocked_determine_block_hash.return_value, + reuse_block_hash=False, + ) + mocked_from_dict.assert_called_once_with(mocked_query.return_value.value[0]) + assert result == mocked_from_dict.return_value + + +@pytest.mark.asyncio +async def test_get_proxy_announcements(subtensor, mocker): + """Test get_proxy_announcements returns correct data when announcement information is found.""" + # Prep + fake_delegate = mocker.Mock(spec=str) + fake_proxies_list = mocker.Mock(spec=list) + mocked_determine_block_hash = mocker.patch.object( + subtensor, "determine_block_hash", return_value="mock_block_hash" + ) + + fake_record = (fake_delegate, fake_proxies_list) + fake_query_map_records = [fake_record] + mocked_query_map_return = mocker.MagicMock(return_value=fake_query_map_records) + mocked_query_map_return.__aiter__.return_value = iter(fake_query_map_records) + + mocked_query_map = mocker.patch.object( + subtensor.substrate, + "query_map", + return_value=mocked_query_map_return, + ) + mocked_from_query_map_record = mocker.patch.object( + async_subtensor.ProxyAnnouncementInfo, + "from_query_map_record", + side_effect=fake_query_map_records, + ) + + # Call + result = await subtensor.get_proxy_announcements() + + # Asserts + mocked_determine_block_hash.assert_awaited_once_with(None, None, False) + mocked_query_map.assert_awaited_once_with( + module="Proxy", + storage_function="Announcements", + block_hash=mocked_determine_block_hash.return_value, + reuse_block_hash=False, + ) + mocked_from_query_map_record.assert_called_once_with(fake_record) + assert result == {fake_delegate: fake_proxies_list} + + +@pytest.mark.asyncio +async def test_get_proxy_constants(subtensor, mocker): + """Test get_proxy_constants returns correct data when constants are found.""" + # Prep + fake_constants = { + "AnnouncementDepositBase": 1000000, + "AnnouncementDepositFactor": 500000, + "MaxProxies": 32, + "MaxPending": 32, + "ProxyDepositBase": 2000000, + "ProxyDepositFactor": 1000000, + } + + mocked_query_constant = mocker.patch.object( + subtensor, + "query_constant", + side_effect=[mocker.Mock(value=value) for value in fake_constants.values()], + ) + mocked_from_dict = mocker.patch.object(async_subtensor.ProxyConstants, "from_dict") + + # Call + result = await subtensor.get_proxy_constants() + + # Asserts + assert mocked_query_constant.call_count == len(fake_constants) + mocked_from_dict.assert_called_once_with(fake_constants) + assert result == mocked_from_dict.return_value + + +@pytest.mark.asyncio +async def test_get_proxy_constants_as_dict(subtensor, mocker): + """Test get_proxy_constants returns dict when as_dict=True.""" + # Prep + fake_constants = { + "AnnouncementDepositBase": 1000000, + "AnnouncementDepositFactor": 500000, + "MaxProxies": 32, + "MaxPending": 32, + "ProxyDepositBase": 2000000, + "ProxyDepositFactor": 1000000, + } + + mocked_query_constant = mocker.patch.object( + subtensor, + "query_constant", + side_effect=[mocker.Mock(value=value) for value in fake_constants.values()], + ) + mocked_proxy_constants = mocker.Mock() + mocked_from_dict = mocker.patch.object( + async_subtensor.ProxyConstants, + "from_dict", + return_value=mocked_proxy_constants, + ) + mocked_to_dict = mocker.patch.object( + mocked_proxy_constants, + "to_dict", + return_value=fake_constants, + ) + + # Call + result = await subtensor.get_proxy_constants(as_dict=True) + + # Asserts + assert mocked_query_constant.call_count == len(fake_constants) + mocked_from_dict.assert_called_once_with(fake_constants) + mocked_to_dict.assert_called_once() + assert result == fake_constants + + +@pytest.mark.asyncio +async def test_add_proxy(mocker, subtensor): + """Tests `add_proxy` extrinsic call method.""" + # preps + wallet = mocker.Mock(spec=Wallet) + delegate_ss58 = mocker.Mock(spec=str) + proxy_type = mocker.Mock(spec=str) + delay = mocker.Mock(spec=int) + mocked_add_proxy_extrinsic = mocker.patch.object( + async_subtensor, "add_proxy_extrinsic" + ) + + # call + response = await subtensor.add_proxy( + wallet=wallet, + delegate_ss58=delegate_ss58, + proxy_type=proxy_type, + delay=delay, + ) + + # asserts + mocked_add_proxy_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + wallet=wallet, + delegate_ss58=delegate_ss58, + proxy_type=proxy_type, + delay=delay, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_add_proxy_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_announce_proxy(mocker, subtensor): + """Tests `announce_proxy` extrinsic call method.""" + # preps + wallet = mocker.Mock(spec=Wallet) + real_account_ss58 = mocker.Mock(spec=str) + call_hash = mocker.Mock(spec=str) + mocked_announce_extrinsic = mocker.patch.object( + async_subtensor, "announce_extrinsic" + ) + + # call + response = await subtensor.announce_proxy( + wallet=wallet, + real_account_ss58=real_account_ss58, + call_hash=call_hash, + ) + + # asserts + mocked_announce_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + wallet=wallet, + real_account_ss58=real_account_ss58, + call_hash=call_hash, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_announce_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_create_pure_proxy(mocker, subtensor): + """Tests `create_pure_proxy` extrinsic call method.""" + # preps + wallet = mocker.Mock(spec=Wallet) + proxy_type = mocker.Mock(spec=str) + delay = mocker.Mock(spec=int) + index = mocker.Mock(spec=int) + mocked_create_pure_proxy_extrinsic = mocker.patch.object( + async_subtensor, "create_pure_proxy_extrinsic" + ) + + # call + response = await subtensor.create_pure_proxy( + wallet=wallet, + proxy_type=proxy_type, + delay=delay, + index=index, + ) + + # asserts + mocked_create_pure_proxy_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + wallet=wallet, + proxy_type=proxy_type, + delay=delay, + index=index, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_create_pure_proxy_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_kill_pure_proxy(mocker, subtensor): + """Tests `kill_pure_proxy` extrinsic call method.""" + # preps + wallet = mocker.Mock(spec=Wallet) + pure_proxy_ss58 = mocker.Mock(spec=str) + spawner = mocker.Mock(spec=str) + proxy_type = mocker.Mock(spec=str) + index = mocker.Mock(spec=int) + height = mocker.Mock(spec=int) + ext_index = mocker.Mock(spec=int) + mocked_kill_pure_proxy_extrinsic = mocker.patch.object( + async_subtensor, "kill_pure_proxy_extrinsic" + ) + + # call + response = await subtensor.kill_pure_proxy( + wallet=wallet, + pure_proxy_ss58=pure_proxy_ss58, + spawner=spawner, + proxy_type=proxy_type, + index=index, + height=height, + ext_index=ext_index, + ) + + # asserts + mocked_kill_pure_proxy_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + wallet=wallet, + pure_proxy_ss58=pure_proxy_ss58, + spawner=spawner, + proxy_type=proxy_type, + index=index, + height=height, + ext_index=ext_index, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_kill_pure_proxy_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_poke_deposit(mocker, subtensor): + """Tests `poke_deposit` extrinsic call method.""" + # preps + wallet = mocker.Mock(spec=Wallet) + mocked_poke_deposit_extrinsic = mocker.patch.object( + async_subtensor, "poke_deposit_extrinsic" + ) + + # call + response = await subtensor.poke_deposit(wallet=wallet) + + # asserts + mocked_poke_deposit_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + wallet=wallet, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_poke_deposit_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_proxy(mocker, subtensor): + """Tests `proxy` extrinsic call method.""" + # preps + wallet = mocker.Mock(spec=Wallet) + real_account_ss58 = mocker.Mock(spec=str) + force_proxy_type = mocker.Mock(spec=str) + call = mocker.Mock(spec=GenericCall) + mocked_proxy_extrinsic = mocker.patch.object(async_subtensor, "proxy_extrinsic") + + # call + response = await subtensor.proxy( + wallet=wallet, + real_account_ss58=real_account_ss58, + force_proxy_type=force_proxy_type, + call=call, + ) + + # asserts + mocked_proxy_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + wallet=wallet, + real_account_ss58=real_account_ss58, + force_proxy_type=force_proxy_type, + call=call, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_proxy_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_proxy_announced(mocker, subtensor): + """Tests `proxy_announced` extrinsic call method.""" + # preps + wallet = mocker.Mock(spec=Wallet) + delegate_ss58 = mocker.Mock(spec=str) + real_account_ss58 = mocker.Mock(spec=str) + force_proxy_type = mocker.Mock(spec=str) + call = mocker.Mock(spec=GenericCall) + mocked_proxy_announced_extrinsic = mocker.patch.object( + async_subtensor, "proxy_announced_extrinsic" + ) + + # call + response = await subtensor.proxy_announced( + wallet=wallet, + delegate_ss58=delegate_ss58, + real_account_ss58=real_account_ss58, + force_proxy_type=force_proxy_type, + call=call, + ) + + # asserts + mocked_proxy_announced_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + wallet=wallet, + delegate_ss58=delegate_ss58, + real_account_ss58=real_account_ss58, + force_proxy_type=force_proxy_type, + call=call, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_proxy_announced_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_reject_proxy_announcement(mocker, subtensor): + """Tests `reject_proxy_announcement` extrinsic call method.""" + # preps + wallet = mocker.Mock(spec=Wallet) + delegate_ss58 = mocker.Mock(spec=str) + call_hash = mocker.Mock(spec=str) + mocked_reject_announcement_extrinsic = mocker.patch.object( + async_subtensor, "reject_announcement_extrinsic" + ) + + # call + response = await subtensor.reject_proxy_announcement( + wallet=wallet, + delegate_ss58=delegate_ss58, + call_hash=call_hash, + ) + + # asserts + mocked_reject_announcement_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + wallet=wallet, + delegate_ss58=delegate_ss58, + call_hash=call_hash, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_reject_announcement_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_remove_proxy_announcement(mocker, subtensor): + """Tests `remove_proxy_announcement` extrinsic call method.""" + # preps + wallet = mocker.Mock(spec=Wallet) + real_account_ss58 = mocker.Mock(spec=str) + call_hash = mocker.Mock(spec=str) + mocked_remove_announcement_extrinsic = mocker.patch.object( + async_subtensor, "remove_announcement_extrinsic" + ) + + # call + response = await subtensor.remove_proxy_announcement( + wallet=wallet, + real_account_ss58=real_account_ss58, + call_hash=call_hash, + ) + + # asserts + mocked_remove_announcement_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + wallet=wallet, + real_account_ss58=real_account_ss58, + call_hash=call_hash, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_remove_announcement_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_remove_proxies(mocker, subtensor): + """Tests `remove_proxies` extrinsic call method.""" + # preps + wallet = mocker.Mock(spec=Wallet) + mocked_remove_proxies_extrinsic = mocker.patch.object( + async_subtensor, "remove_proxies_extrinsic" + ) + + # call + response = await subtensor.remove_proxies(wallet=wallet) + + # asserts + mocked_remove_proxies_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + wallet=wallet, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_remove_proxies_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_remove_proxy(mocker, subtensor): + """Tests `remove_proxy` extrinsic call method.""" + # preps + wallet = mocker.Mock(spec=Wallet) + delegate_ss58 = mocker.Mock(spec=str) + proxy_type = mocker.Mock(spec=str) + delay = mocker.Mock(spec=int) + mocked_remove_proxy_extrinsic = mocker.patch.object( + async_subtensor, "remove_proxy_extrinsic" + ) + + # call + response = await subtensor.remove_proxy( + wallet=wallet, + delegate_ss58=delegate_ss58, + proxy_type=proxy_type, + delay=delay, + ) + + # asserts + mocked_remove_proxy_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + wallet=wallet, + delegate_ss58=delegate_ss58, + proxy_type=proxy_type, + delay=delay, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_remove_proxy_extrinsic.return_value From 1a902d2bebeac7e4d47fc9ef38b13f397db5cc1f Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 10 Nov 2025 13:23:19 -0800 Subject: [PATCH 43/62] extend `poke_deposit` explanation --- bittensor/core/async_subtensor.py | 5 +++++ bittensor/core/extrinsics/asyncex/proxy.py | 5 +++++ bittensor/core/extrinsics/proxy.py | 5 +++++ bittensor/core/subtensor.py | 6 ++++++ 4 files changed, 21 insertions(+) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 4c8e963da5..f2f3a7fd7c 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -6213,6 +6213,11 @@ async def poke_deposit( Note: This method automatically adjusts deposits for both proxy relationships and announcements. No parameters are needed as it operates on the account's current state. + + When to use: + - After runtime upgrade, if deposit constants have changed. + - After removing proxies/announcements, to free up excess locked funds. + - Periodically to optimize locked deposit amounts. """ return await poke_deposit_extrinsic( subtensor=self, diff --git a/bittensor/core/extrinsics/asyncex/proxy.py b/bittensor/core/extrinsics/asyncex/proxy.py index 85c96e4b46..aabb549ec4 100644 --- a/bittensor/core/extrinsics/asyncex/proxy.py +++ b/bittensor/core/extrinsics/asyncex/proxy.py @@ -819,6 +819,11 @@ async def poke_deposit_extrinsic( Returns: ExtrinsicResponse: The result object of the extrinsic execution. + + When to use: + - After runtime upgrade, if deposit constants have changed. + - After removing proxies/announcements, to free up excess locked funds. + - Periodically to optimize locked deposit amounts. """ try: if not ( diff --git a/bittensor/core/extrinsics/proxy.py b/bittensor/core/extrinsics/proxy.py index 3d7db6925c..bc32099e96 100644 --- a/bittensor/core/extrinsics/proxy.py +++ b/bittensor/core/extrinsics/proxy.py @@ -818,6 +818,11 @@ def poke_deposit_extrinsic( Returns: ExtrinsicResponse: The result object of the extrinsic execution. + + When to use: + - After runtime upgrade, if deposit constants have changed. + - After removing proxies/announcements, to free up excess locked funds. + - Periodically to optimize locked deposit amounts. """ try: if not ( diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 37b5a16817..917bd98033 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -4921,6 +4921,12 @@ def poke_deposit( Note: This method automatically adjusts deposits for both proxy relationships and announcements. No parameters are needed as it operates on the account's current state. + + When to use: + - After runtime upgrade, if deposit constants have changed. + - After removing proxies/announcements, to free up excess locked funds. + - Periodically to optimize locked deposit amounts. + """ return poke_deposit_extrinsic( subtensor=self, From b7849598c8b09ae35743ee6d2cf221bc499a11c2 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 10 Nov 2025 13:45:10 -0800 Subject: [PATCH 44/62] ruff + docstrings --- bittensor/core/async_subtensor.py | 8 ++--- bittensor/core/extrinsics/asyncex/proxy.py | 39 ++++++++++---------- bittensor/core/extrinsics/proxy.py | 41 ++++++++++------------ bittensor/core/subtensor.py | 9 +++-- 4 files changed, 45 insertions(+), 52 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index f2f3a7fd7c..4c81fafbbb 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -6249,8 +6249,8 @@ async def proxy( Parameters: wallet: Bittensor wallet object (should be the proxy account wallet). real_account_ss58: The SS58 address of the real account on whose behalf the call is being made. - force_proxy_type: The type of proxy to use for the call. If None, any proxy type can be used. Otherwise, must - match one of the allowed proxy types. Can be a string or ProxyType enum value. + force_proxy_type: The type of proxy to use for the call. If None, any proxy type can be used. Otherwise, + must match one of the allowed proxy types. Can be a string or ProxyType enum value. call: The inner call to be executed on behalf of the real account. 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 @@ -6301,8 +6301,8 @@ async def proxy_announced( wallet: Bittensor wallet object (should be the proxy account wallet that made the announcement). delegate_ss58: The SS58 address of the delegate proxy account that made the announcement. real_account_ss58: The SS58 address of the real account on whose behalf the call will be made. - force_proxy_type: The type of proxy to use for the call. If None, any proxy type can be used. Otherwise, must - match one of the allowed proxy types. Can be a string or ProxyType enum value. + force_proxy_type: The type of proxy to use for the call. If None, any proxy type can be used. Otherwise, + must match one of the allowed proxy types. Can be a string or ProxyType enum value. call: The inner call to be executed on behalf of the real account (must match the announced call_hash). 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 diff --git a/bittensor/core/extrinsics/asyncex/proxy.py b/bittensor/core/extrinsics/asyncex/proxy.py index aabb549ec4..4f14dd4406 100644 --- a/bittensor/core/extrinsics/asyncex/proxy.py +++ b/bittensor/core/extrinsics/asyncex/proxy.py @@ -316,29 +316,27 @@ async def kill_pure_proxy_extrinsic( """ Kills (removes) a pure proxy account. - This method removes a pure proxy account that was previously created via `create_pure_proxy()`. - The `kill_pure` call must be executed through the pure proxy account itself, with the spawner - acting as an "Any" proxy. This method automatically handles this by executing the call via - `proxy()`. + This method removes a pure proxy account that was previously created via `create_pure_proxy()`. The `kill_pure` call + must be executed through the pure proxy account itself, with the spawner acting as an "Any" proxy. This method + automatically handles this by executing the call via `proxy()`. Parameters: subtensor: Subtensor instance with the connection to the chain. - wallet: Bittensor wallet object. The wallet.coldkey.ss58_address must be the spawner of - the pure proxy (the account that created it via `create_pure_proxy()`). The spawner - must have an "Any" proxy relationship with the pure proxy. - pure_proxy_ss58: The SS58 address of the pure proxy account to be killed. This is the - address that was returned in the `create_pure_proxy()` response. - spawner: The SS58 address of the spawner account (the account that originally created - the pure proxy via `create_pure_proxy()`). This should match wallet.coldkey.ss58_address. - proxy_type: The type of proxy permissions. Can be a string or ProxyType enum value. Must - match the proxy_type used when creating the pure proxy. + wallet: Bittensor wallet object. The wallet.coldkey.ss58_address must be the spawner of the pure proxy (the + account that created it via `create_pure_proxy()`). The spawner must have an "Any" proxy relationship with + the pure proxy. + pure_proxy_ss58: The SS58 address of the pure proxy account to be killed. This is the address that was returned + in the `create_pure_proxy()` response. + spawner: The SS58 address of the spawner account (the account that originally created the pure proxy via + `create_pure_proxy()`). This should match wallet.coldkey.ss58_address. + proxy_type: The type of proxy permissions. Can be a string or ProxyType enum value. Must match the proxy_type + used when creating the pure proxy. index: The disambiguation index originally passed to `create_pure()`. height: The block height at which the pure proxy was created. ext_index: The extrinsic index at which the pure proxy was created. - 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. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You can + think of it as an expiration date for the transaction. raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. wait_for_inclusion: Whether to wait for the inclusion of the transaction. wait_for_finalization: Whether to wait for the finalization of the transaction. @@ -347,10 +345,9 @@ async def kill_pure_proxy_extrinsic( ExtrinsicResponse: The result object of the extrinsic execution. Note: - The `kill_pure` call must be executed through the pure proxy account itself, with the - spawner acting as an "Any" proxy. This method automatically handles this by executing - the call via `proxy()`. The spawner must have an "Any" proxy relationship with the pure - proxy for this to work. + The `kill_pure` call must be executed through the pure proxy account itself, with the spawner acting as an "Any" + proxy. This method automatically handles this by executing the call via `proxy()`. The spawner must have an + "Any" proxy relationship with the pure proxy for this to work. Example: # After creating a pure proxy diff --git a/bittensor/core/extrinsics/proxy.py b/bittensor/core/extrinsics/proxy.py index bc32099e96..a5a27d4753 100644 --- a/bittensor/core/extrinsics/proxy.py +++ b/bittensor/core/extrinsics/proxy.py @@ -315,29 +315,27 @@ def kill_pure_proxy_extrinsic( """ Kills (removes) a pure proxy account. - This method removes a pure proxy account that was previously created via `create_pure_proxy()`. - The `kill_pure` call must be executed through the pure proxy account itself, with the spawner - acting as an "Any" proxy. This method automatically handles this by executing the call via - `proxy()`. + This method removes a pure proxy account that was previously created via `create_pure_proxy()`. The `kill_pure` call + must be executed through the pure proxy account itself, with the spawner acting as an "Any" proxy. This method + automatically handles this by executing the call via `proxy()`. Parameters: subtensor: Subtensor instance with the connection to the chain. - wallet: Bittensor wallet object. The wallet.coldkey.ss58_address must be the spawner of - the pure proxy (the account that created it via `create_pure_proxy()`). The spawner - must have an "Any" proxy relationship with the pure proxy. - pure_proxy_ss58: The SS58 address of the pure proxy account to be killed. This is the - address that was returned in the `create_pure_proxy()` response. - spawner: The SS58 address of the spawner account (the account that originally created - the pure proxy via `create_pure_proxy()`). This should match wallet.coldkey.ss58_address. - proxy_type: The type of proxy permissions. Can be a string or ProxyType enum value. Must - match the proxy_type used when creating the pure proxy. + wallet: Bittensor wallet object. The wallet.coldkey.ss58_address must be the spawner of the pure proxy (the + account that created it via `create_pure_proxy()`). The spawner must have an "Any" proxy relationship with + the pure proxy. + pure_proxy_ss58: The SS58 address of the pure proxy account to be killed. This is the address that was returned + in the `create_pure_proxy()` response. + spawner: The SS58 address of the spawner account (the account that originally created the pure proxy via + `create_pure_proxy()`). This should match wallet.coldkey.ss58_address. + proxy_type: The type of proxy permissions. Can be a string or ProxyType enum value. Must match the proxy_type + used when creating the pure proxy. index: The disambiguation index originally passed to `create_pure()`. height: The block height at which the pure proxy was created. ext_index: The extrinsic index at which the pure proxy was created. - 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. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You can + think of it as an expiration date for the transaction. raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. wait_for_inclusion: Whether to wait for the inclusion of the transaction. wait_for_finalization: Whether to wait for the finalization of the transaction. @@ -346,10 +344,9 @@ def kill_pure_proxy_extrinsic( ExtrinsicResponse: The result object of the extrinsic execution. Note: - The `kill_pure` call must be executed through the pure proxy account itself, with the - spawner acting as an "Any" proxy. This method automatically handles this by executing - the call via `proxy()`. The spawner must have an "Any" proxy relationship with the pure - proxy for this to work. + The `kill_pure` call must be executed through the pure proxy account itself, with the spawner acting as an "Any" + proxy. This method automatically handles this by executing the call via `proxy()`. The spawner must have an + "Any" proxy relationship with the pure proxy for this to work. Example: # After creating a pure proxy @@ -417,7 +414,7 @@ def kill_pure_proxy_extrinsic( subtensor=subtensor, wallet=wallet, real_account_ss58=pure_proxy_ss58, - force_proxy_type=ProxyType.Any, + force_proxy_type=proxy_type_str, call=kill_pure_call, period=period, raise_error=raise_error, diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 917bd98033..20c0b45b53 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -4926,7 +4926,6 @@ def poke_deposit( - After runtime upgrade, if deposit constants have changed. - After removing proxies/announcements, to free up excess locked funds. - Periodically to optimize locked deposit amounts. - """ return poke_deposit_extrinsic( subtensor=self, @@ -4958,8 +4957,8 @@ def proxy( Parameters: wallet: Bittensor wallet object (should be the proxy account wallet). real_account_ss58: The SS58 address of the real account on whose behalf the call is being made. - force_proxy_type: The type of proxy to use for the call. If None, any proxy type can be used. Otherwise, must - match one of the allowed proxy types. Can be a string or ProxyType enum value. + force_proxy_type: The type of proxy to use for the call. If None, any proxy type can be used. Otherwise, + must match one of the allowed proxy types. Can be a string or ProxyType enum value. call: The inner call to be executed on behalf of the real account. 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 @@ -5010,8 +5009,8 @@ def proxy_announced( wallet: Bittensor wallet object (should be the proxy account wallet that made the announcement). delegate_ss58: The SS58 address of the delegate proxy account that made the announcement. real_account_ss58: The SS58 address of the real account on whose behalf the call will be made. - force_proxy_type: The type of proxy to use for the call. If None, any proxy type can be used. Otherwise, must - match one of the allowed proxy types. Can be a string or ProxyType enum value. + force_proxy_type: The type of proxy to use for the call. If None, any proxy type can be used. Otherwise, + must match one of the allowed proxy types. Can be a string or ProxyType enum value. call: The inner call to be executed on behalf of the real account (must match the announced call_hash). 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 From 61789479aca92129a91337472c53243d9a7d4e60 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 10 Nov 2025 14:10:25 -0800 Subject: [PATCH 45/62] `ProxyType.Any` for kill proxy --- bittensor/core/extrinsics/proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/core/extrinsics/proxy.py b/bittensor/core/extrinsics/proxy.py index a5a27d4753..1d42224fb3 100644 --- a/bittensor/core/extrinsics/proxy.py +++ b/bittensor/core/extrinsics/proxy.py @@ -414,7 +414,7 @@ def kill_pure_proxy_extrinsic( subtensor=subtensor, wallet=wallet, real_account_ss58=pure_proxy_ss58, - force_proxy_type=proxy_type_str, + force_proxy_type=ProxyType.Any, call=kill_pure_call, period=period, raise_error=raise_error, From 03d0de3a01bdd48453eb99eb5e5758c7abb1cc0a Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 10 Nov 2025 14:10:37 -0800 Subject: [PATCH 46/62] fix SubtensorApi unit test --- tests/unit_tests/test_subtensor_api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit_tests/test_subtensor_api.py b/tests/unit_tests/test_subtensor_api.py index c16f8c59c2..7985959aba 100644 --- a/tests/unit_tests/test_subtensor_api.py +++ b/tests/unit_tests/test_subtensor_api.py @@ -36,6 +36,7 @@ def test_properties_methods_comparable(other_class: "Subtensor" = None): m for m in dir(subtensor_api.metagraphs) if not m.startswith("_") ] neurons_methods = [m for m in dir(subtensor_api.neurons) if not m.startswith("_")] + proxies_methods = [m for m in dir(subtensor_api.proxies) if not m.startswith("_")] queries_methods = [m for m in dir(subtensor_api.queries) if not m.startswith("_")] stakes_methods = [m for m in dir(subtensor_api.staking) if not m.startswith("_")] subnets_methods = [m for m in dir(subtensor_api.subnets) if not m.startswith("_")] @@ -50,6 +51,7 @@ def test_properties_methods_comparable(other_class: "Subtensor" = None): + extrinsics_methods + metagraphs_methods + neurons_methods + + proxies_methods + queries_methods + stakes_methods + subnets_methods From fbd5958bf9a664640b07ef865dbc1ddc86fdbe3c Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 10 Nov 2025 14:12:38 -0800 Subject: [PATCH 47/62] wrong `c` --- .../__init__.py" => tests/consistency/__init__.py | 0 .../conftest.py" => tests/consistency/conftest.py | 0 .../test_proxy_types.py" => tests/consistency/test_proxy_types.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename "tests/\321\201onsistency/__init__.py" => tests/consistency/__init__.py (100%) rename "tests/\321\201onsistency/conftest.py" => tests/consistency/conftest.py (100%) rename "tests/\321\201onsistency/test_proxy_types.py" => tests/consistency/test_proxy_types.py (100%) diff --git "a/tests/\321\201onsistency/__init__.py" b/tests/consistency/__init__.py similarity index 100% rename from "tests/\321\201onsistency/__init__.py" rename to tests/consistency/__init__.py diff --git "a/tests/\321\201onsistency/conftest.py" b/tests/consistency/conftest.py similarity index 100% rename from "tests/\321\201onsistency/conftest.py" rename to tests/consistency/conftest.py diff --git "a/tests/\321\201onsistency/test_proxy_types.py" b/tests/consistency/test_proxy_types.py similarity index 100% rename from "tests/\321\201onsistency/test_proxy_types.py" rename to tests/consistency/test_proxy_types.py From 326ef1a1b5bb17be707db7f9c063c586e56ddb73 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 10 Nov 2025 14:19:40 -0800 Subject: [PATCH 48/62] matrix.test-file.nodeid --- .github/workflows/subtensor-consistency-tests.yaml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/subtensor-consistency-tests.yaml b/.github/workflows/subtensor-consistency-tests.yaml index ebf7256422..aa1408bee8 100644 --- a/.github/workflows/subtensor-consistency-tests.yaml +++ b/.github/workflows/subtensor-consistency-tests.yaml @@ -65,7 +65,6 @@ jobs: | jq -R -s -c ' split("\n") | map(select(. != "")) - | map({nodeid: ., label: (sub("^tests/consistency/"; ""))}) ' ) echo "Found tests: $test_matrix" @@ -82,16 +81,16 @@ jobs: run: | echo "Event: $GITHUB_EVENT_NAME" echo "Branch: $GITHUB_REF_NAME" - + echo "Reading labels ..." if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then labels=$(jq -r '.pull_request.labels[].name' "$GITHUB_EVENT_PATH") else labels="" fi - + image="" - + for label in $labels; do echo "Found label: $label" case "$label" in @@ -109,7 +108,7 @@ jobs: ;; esac done - + if [[ -z "$image" ]]; then # fallback to default based on branch if [[ "${GITHUB_REF_NAME}" == "master" ]]; then @@ -118,7 +117,7 @@ jobs: image="ghcr.io/opentensor/subtensor-localnet:devnet-ready" fi fi - + echo "✅ Final selected image: $image" echo "image=$image" >> "$GITHUB_OUTPUT" @@ -202,7 +201,7 @@ jobs: set +e for i in 1 2 3; do echo "::group::🔁 Test attempt $i" - uv run pytest ${{ matrix.test-file }} -s + uv run pytest ${{ matrix.test-file.nodeid }} -s status=$? if [ $status -eq 0 ]; then echo "✅ Tests passed on attempt $i" From bd08bcccb25e85807dbb41dffb2c4fd338987e94 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 10 Nov 2025 14:32:56 -0800 Subject: [PATCH 49/62] weird run --- .github/workflows/subtensor-consistency-tests.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/subtensor-consistency-tests.yaml b/.github/workflows/subtensor-consistency-tests.yaml index aa1408bee8..4e68741386 100644 --- a/.github/workflows/subtensor-consistency-tests.yaml +++ b/.github/workflows/subtensor-consistency-tests.yaml @@ -65,6 +65,7 @@ jobs: | jq -R -s -c ' split("\n") | map(select(. != "")) + | map({nodeid: ., label: (sub("^tests/consistency/"; ""))}) ' ) echo "Found tests: $test_matrix" @@ -142,7 +143,7 @@ jobs: # Python versions. To reduce DRY we use reusable workflow. consistency-tests: - name: "Consistency test: ${{ matrix.test-file }}" + name: "Consistency test: ${{ matrix.test-file.label }}" needs: - find-tests - pull-docker-image From fa5dd286c32610b0584126f0229d4c00aa76f358 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 10 Nov 2025 14:39:20 -0800 Subject: [PATCH 50/62] weird ref --- .github/workflows/subtensor-consistency-tests.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/subtensor-consistency-tests.yaml b/.github/workflows/subtensor-consistency-tests.yaml index 4e68741386..b7e02abf4d 100644 --- a/.github/workflows/subtensor-consistency-tests.yaml +++ b/.github/workflows/subtensor-consistency-tests.yaml @@ -162,8 +162,6 @@ jobs: steps: - name: Check-out repository uses: actions/checkout@v4 - with: - ref: master - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 From fad5984047485d48745f66e4cfebc10bcc8b62b0 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 10 Nov 2025 14:49:43 -0800 Subject: [PATCH 51/62] `CallBuilder.dynamic_function = True` by default --- bittensor/core/extrinsics/pallets/base.py | 2 +- tests/e2e_tests/test_subtensor_functions.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/bittensor/core/extrinsics/pallets/base.py b/bittensor/core/extrinsics/pallets/base.py index f8a94a85a3..99b5640d85 100644 --- a/bittensor/core/extrinsics/pallets/base.py +++ b/bittensor/core/extrinsics/pallets/base.py @@ -28,7 +28,7 @@ class CallBuilder: """ subtensor: Union["Subtensor", "AsyncSubtensor"] - dynamic_function: bool = False + dynamic_function: bool = True def create_composed_call( self, call_module: str = None, call_function: str = None, **kwargs diff --git a/tests/e2e_tests/test_subtensor_functions.py b/tests/e2e_tests/test_subtensor_functions.py index d772f056d0..1783a44e56 100644 --- a/tests/e2e_tests/test_subtensor_functions.py +++ b/tests/e2e_tests/test_subtensor_functions.py @@ -508,6 +508,15 @@ class SubtensorModule(CallBuilder): ... neurons = subtensor.neurons.neurons(netuid) assert len(neurons) == 1, "No neurons found or more than one." + # create call with wrong function name + with pytest.raises(ValueError): + CallBuilder(subtensor, dynamic_function=False).create_composed_call( + call_module="SubtensorModule", + call_function="some_call_function", + netuid=netuid, + hotkey=bob_wallet.hotkey.ss58_address, + ) + # create call dynamically burned_register_call = CallBuilder( subtensor, dynamic_function=True @@ -581,6 +590,15 @@ class SubtensorModule(CallBuilder): ... neurons = await async_subtensor.neurons.neurons(netuid) assert len(neurons) == 1, "No neurons found or more than one." + # create call with wrong function name + with pytest.raises(ValueError): + await CallBuilder(async_subtensor, dynamic_function=False).create_composed_call( + call_module="SubtensorModule", + call_function="some_call_function", + netuid=netuid, + hotkey=bob_wallet.hotkey.ss58_address, + ) + # create call dynamically burned_register_call = await CallBuilder( async_subtensor, dynamic_function=True From 6b6b6d8845fb879f5f03308f92002d9a6f7aa7e6 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 10 Nov 2025 15:48:53 -0800 Subject: [PATCH 52/62] apply `0x` to call_hash in announce related extrinsics --- bittensor/core/extrinsics/asyncex/proxy.py | 9 +++++++++ bittensor/core/extrinsics/proxy.py | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/bittensor/core/extrinsics/asyncex/proxy.py b/bittensor/core/extrinsics/asyncex/proxy.py index 4f14dd4406..afc98ae3ee 100644 --- a/bittensor/core/extrinsics/asyncex/proxy.py +++ b/bittensor/core/extrinsics/asyncex/proxy.py @@ -624,6 +624,9 @@ async def announce_extrinsic( ).success: return unlocked + # make sure the call hash starts with 0x + call_hash = "0x" + call_hash.lstrip("0x") + logging.debug( f"Announcing proxy call: real=[blue]{real_account_ss58}[/blue], " f"call_hash=[blue]{call_hash}[/blue] " @@ -692,6 +695,9 @@ async def reject_announcement_extrinsic( ).success: return unlocked + # make sure the call hash starts with 0x + call_hash = "0x" + call_hash.lstrip("0x") + logging.debug( f"Rejecting announcement: delegate=[blue]{delegate_ss58}[/blue], " f"call_hash=[blue]{call_hash}[/blue] " @@ -760,6 +766,9 @@ async def remove_announcement_extrinsic( ).success: return unlocked + # make sure the call hash starts with 0x + call_hash = "0x" + call_hash.lstrip("0x") + logging.debug( f"Removing announcement: real=[blue]{real_account_ss58}[/blue], " f"call_hash=[blue]{call_hash}[/blue] " diff --git a/bittensor/core/extrinsics/proxy.py b/bittensor/core/extrinsics/proxy.py index 1d42224fb3..df387c2bd3 100644 --- a/bittensor/core/extrinsics/proxy.py +++ b/bittensor/core/extrinsics/proxy.py @@ -623,6 +623,9 @@ def announce_extrinsic( ).success: return unlocked + # make sure the call hash starts with 0x + call_hash = "0x" + call_hash.lstrip("0x") + logging.debug( f"Announcing proxy call: real=[blue]{real_account_ss58}[/blue], " f"call_hash=[blue]{call_hash}[/blue] " @@ -691,6 +694,9 @@ def reject_announcement_extrinsic( ).success: return unlocked + # make sure the call hash starts with 0x + call_hash = "0x" + call_hash.lstrip("0x") + logging.debug( f"Rejecting announcement: delegate=[blue]{delegate_ss58}[/blue], " f"call_hash=[blue]{call_hash}[/blue] " @@ -759,6 +765,9 @@ def remove_announcement_extrinsic( ).success: return unlocked + # make sure the call hash starts with 0x + call_hash = "0x" + call_hash.lstrip("0x") + logging.debug( f"Removing announcement: real=[blue]{real_account_ss58}[/blue], " f"call_hash=[blue]{call_hash}[/blue] " From 5e03f7b4c278df436c48cea6d2e2042e760f36e4 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Mon, 10 Nov 2025 16:19:39 -0800 Subject: [PATCH 53/62] unit tests --- tests/unit_tests/extrinsics/test_proxy.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit_tests/extrinsics/test_proxy.py b/tests/unit_tests/extrinsics/test_proxy.py index 13fff48a14..2bff4fcb21 100644 --- a/tests/unit_tests/extrinsics/test_proxy.py +++ b/tests/unit_tests/extrinsics/test_proxy.py @@ -465,7 +465,7 @@ def test_announce_extrinsic(subtensor, mocker): # Asserts mocked_pallet_call.assert_called_once_with( real=real_account_ss58, - call_hash=call_hash, + call_hash=call_hash.lstrip().__radd__(), ) mocked_sign_and_send_extrinsic.assert_called_once_with( call=mocked_pallet_call.return_value, @@ -501,7 +501,7 @@ def test_reject_announcement_extrinsic(subtensor, mocker): # Asserts mocked_pallet_call.assert_called_once_with( delegate=delegate_ss58, - call_hash=call_hash, + call_hash=call_hash.lstrip().__radd__(), ) mocked_sign_and_send_extrinsic.assert_called_once_with( call=mocked_pallet_call.return_value, @@ -537,7 +537,7 @@ def test_remove_announcement_extrinsic(subtensor, mocker): # Asserts mocked_pallet_call.assert_called_once_with( real=real_account_ss58, - call_hash=call_hash, + call_hash=call_hash.lstrip().__radd__(), ) mocked_sign_and_send_extrinsic.assert_called_once_with( call=mocked_pallet_call.return_value, From 205b6872c9a7c8f4a4654680bba9f8e84030860d Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Tue, 11 Nov 2025 12:42:20 -0800 Subject: [PATCH 54/62] improve docstring --- bittensor/core/chain_data/proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/core/chain_data/proxy.py b/bittensor/core/chain_data/proxy.py index 456b92f388..f8735f6e95 100644 --- a/bittensor/core/chain_data/proxy.py +++ b/bittensor/core/chain_data/proxy.py @@ -201,7 +201,7 @@ def from_query(cls, query: Any) -> tuple[list["ProxyInfo"], Balance]: Creates a list of ProxyInfo objects and deposit balance from a Substrate query result. Parameters: - query: Query result from Substrate containing proxy data structure. + query: Query result from Substrate `query()` call to `Proxy.Proxies` storage function. Returns: Tuple containing: From e24ff693620ab3f1ec47cecd09850dafa61577b4 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Tue, 11 Nov 2025 12:43:22 -0800 Subject: [PATCH 55/62] add `force_proxy_type` parameter to extrinsic calls + improve the docstring --- bittensor/core/extrinsics/asyncex/proxy.py | 34 +++++++++++++++------- bittensor/core/extrinsics/proxy.py | 30 +++++++++++++------ 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/bittensor/core/extrinsics/asyncex/proxy.py b/bittensor/core/extrinsics/asyncex/proxy.py index afc98ae3ee..84b52262fa 100644 --- a/bittensor/core/extrinsics/asyncex/proxy.py +++ b/bittensor/core/extrinsics/asyncex/proxy.py @@ -308,6 +308,7 @@ async def kill_pure_proxy_extrinsic( index: int, height: int, ext_index: int, + force_proxy_type: Optional[Union[str, ProxyType]] = ProxyType.Any, period: Optional[int] = None, raise_error: bool = False, wait_for_inclusion: bool = True, @@ -329,11 +330,17 @@ async def kill_pure_proxy_extrinsic( in the `create_pure_proxy()` response. spawner: The SS58 address of the spawner account (the account that originally created the pure proxy via `create_pure_proxy()`). This should match wallet.coldkey.ss58_address. - proxy_type: The type of proxy permissions. Can be a string or ProxyType enum value. Must match the proxy_type - used when creating the pure proxy. + proxy_type: The type of proxy permissions that were used when creating the pure proxy. This must match exactly + the proxy_type that was passed to `create_pure_proxy()`. index: The disambiguation index originally passed to `create_pure()`. height: The block height at which the pure proxy was created. ext_index: The extrinsic index at which the pure proxy was created. + force_proxy_type: The proxy type relationship to use when executing `kill_pure` through the proxy mechanism. + Since pure proxies are keyless and cannot sign transactions, the spawner must act as a proxy for the pure + proxy to execute `kill_pure`. This parameter specifies which proxy type relationship between the spawner and + the pure proxy account should be used. The spawner must have a proxy relationship of this type (or `Any`) + with the pure proxy account. Defaults to `ProxyType.Any` for maximum compatibility. If `None`, Substrate + will automatically select an available proxy type from the spawner's proxy relationships. 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. @@ -345,32 +352,37 @@ async def kill_pure_proxy_extrinsic( ExtrinsicResponse: The result object of the extrinsic execution. Note: - The `kill_pure` call must be executed through the pure proxy account itself, with the spawner acting as an "Any" - proxy. This method automatically handles this by executing the call via `proxy()`. The spawner must have an - "Any" proxy relationship with the pure proxy for this to work. + The `kill_pure` call must be executed through the pure proxy account itself, with the spawner acting as a proxy. + This method automatically handles this by executing the call via `proxy()`. By default, `force_proxy_type` is + set to `ProxyType.Any`, meaning the spawner must have an "Any" proxy relationship with the pure proxy. If you + pass a different `force_proxy_type`, the spawner must have that specific proxy type relationship with the pure + proxy. Example: # After creating a pure proxy - create_response = await subtensor.proxies.create_pure_proxy( + create_response = subtensor.proxies.create_pure_proxy( wallet=spawner_wallet, - proxy_type=ProxyType.Any, + proxy_type=ProxyType.Any, # Type of proxy permissions for the pure proxy delay=0, index=0, ) pure_proxy_ss58 = create_response.data["pure_account"] spawner = create_response.data["spawner"] + proxy_type_used = create_response.data["proxy_type"] # The proxy_type used during creation height = create_response.data["height"] ext_index = create_response.data["ext_index"] # Kill the pure proxy - kill_response = await subtensor.proxies.kill_pure_proxy( + # Note: force_proxy_type defaults to ProxyType.Any (spawner must have Any proxy relationship) + kill_response = subtensor.proxies.kill_pure_proxy( wallet=spawner_wallet, pure_proxy_ss58=pure_proxy_ss58, spawner=spawner, - proxy_type=ProxyType.Any, + proxy_type=proxy_type_used, # Must match the proxy_type used during creation index=0, height=height, ext_index=ext_index, + # force_proxy_type=ProxyType.Any, # Optional: defaults to ProxyType.Any ) """ try: @@ -410,12 +422,12 @@ async def kill_pure_proxy_extrinsic( # Execute kill_pure through proxy() where: # - wallet (spawner) signs the transaction # - real_account_ss58 (pure_proxy_ss58) is the origin (pure proxy account) - # - force_proxy_type=Any (spawner acts as Any proxy for pure proxy) + # - force_proxy_type (defaults to ProxyType.Any, spawner acts as proxy for pure proxy) response = await proxy_extrinsic( subtensor=subtensor, wallet=wallet, real_account_ss58=pure_proxy_ss58, - force_proxy_type=ProxyType.Any, + force_proxy_type=force_proxy_type, call=kill_pure_call, period=period, raise_error=raise_error, diff --git a/bittensor/core/extrinsics/proxy.py b/bittensor/core/extrinsics/proxy.py index df387c2bd3..cb3c9736cc 100644 --- a/bittensor/core/extrinsics/proxy.py +++ b/bittensor/core/extrinsics/proxy.py @@ -307,6 +307,7 @@ def kill_pure_proxy_extrinsic( index: int, height: int, ext_index: int, + force_proxy_type: Optional[Union[str, ProxyType]] = ProxyType.Any, period: Optional[int] = None, raise_error: bool = False, wait_for_inclusion: bool = True, @@ -328,11 +329,17 @@ def kill_pure_proxy_extrinsic( in the `create_pure_proxy()` response. spawner: The SS58 address of the spawner account (the account that originally created the pure proxy via `create_pure_proxy()`). This should match wallet.coldkey.ss58_address. - proxy_type: The type of proxy permissions. Can be a string or ProxyType enum value. Must match the proxy_type - used when creating the pure proxy. + proxy_type: The type of proxy permissions that were used when creating the pure proxy. This must match exactly + the proxy_type that was passed to `create_pure_proxy()`. index: The disambiguation index originally passed to `create_pure()`. height: The block height at which the pure proxy was created. ext_index: The extrinsic index at which the pure proxy was created. + force_proxy_type: The proxy type relationship to use when executing `kill_pure` through the proxy mechanism. + Since pure proxies are keyless and cannot sign transactions, the spawner must act as a proxy for the pure + proxy to execute `kill_pure`. This parameter specifies which proxy type relationship between the spawner and + the pure proxy account should be used. The spawner must have a proxy relationship of this type (or `Any`) + with the pure proxy account. Defaults to `ProxyType.Any` for maximum compatibility. If `None`, Substrate + will automatically select an available proxy type from the spawner's proxy relationships. 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. @@ -344,32 +351,37 @@ def kill_pure_proxy_extrinsic( ExtrinsicResponse: The result object of the extrinsic execution. Note: - The `kill_pure` call must be executed through the pure proxy account itself, with the spawner acting as an "Any" - proxy. This method automatically handles this by executing the call via `proxy()`. The spawner must have an - "Any" proxy relationship with the pure proxy for this to work. + The `kill_pure` call must be executed through the pure proxy account itself, with the spawner acting as a proxy. + This method automatically handles this by executing the call via `proxy()`. By default, `force_proxy_type` is + set to `ProxyType.Any`, meaning the spawner must have an "Any" proxy relationship with the pure proxy. If you + pass a different `force_proxy_type`, the spawner must have that specific proxy type relationship with the pure + proxy. Example: # After creating a pure proxy create_response = subtensor.proxies.create_pure_proxy( wallet=spawner_wallet, - proxy_type=ProxyType.Any, + proxy_type=ProxyType.Any, # Type of proxy permissions for the pure proxy delay=0, index=0, ) pure_proxy_ss58 = create_response.data["pure_account"] spawner = create_response.data["spawner"] + proxy_type_used = create_response.data["proxy_type"] # The proxy_type used during creation height = create_response.data["height"] ext_index = create_response.data["ext_index"] # Kill the pure proxy + # Note: force_proxy_type defaults to ProxyType.Any (spawner must have Any proxy relationship) kill_response = subtensor.proxies.kill_pure_proxy( wallet=spawner_wallet, pure_proxy_ss58=pure_proxy_ss58, spawner=spawner, - proxy_type=ProxyType.Any, + proxy_type=proxy_type_used, # Must match the proxy_type used during creation index=0, height=height, ext_index=ext_index, + # force_proxy_type=ProxyType.Any, # Optional: defaults to ProxyType.Any ) """ try: @@ -409,12 +421,12 @@ def kill_pure_proxy_extrinsic( # Execute kill_pure through proxy() where: # - wallet (spawner) signs the transaction # - real_account_ss58 (pure_proxy_ss58) is the origin (pure proxy account) - # - force_proxy_type=Any (spawner acts as Any proxy for pure proxy) + # - force_proxy_type (defaults to ProxyType.Any, spawner acts as proxy for pure proxy) response = proxy_extrinsic( subtensor=subtensor, wallet=wallet, real_account_ss58=pure_proxy_ss58, - force_proxy_type=ProxyType.Any, + force_proxy_type=force_proxy_type, call=kill_pure_call, period=period, raise_error=raise_error, From 87edb035f38b68361609f7cb8035808ec592a96b Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Tue, 11 Nov 2025 12:43:35 -0800 Subject: [PATCH 56/62] remove comment --- bittensor/core/subtensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 20c0b45b53..e2d80280bb 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -4758,7 +4758,7 @@ def kill_pure_proxy( return kill_pure_proxy_extrinsic( subtensor=self, wallet=wallet, - pure_proxy_ss58=pure_proxy_ss58, # НОВЫЙ ПАРАМЕТР + pure_proxy_ss58=pure_proxy_ss58, spawner=spawner, proxy_type=proxy_type, index=index, From 2f7fd8fbad472575feae85dc5d301e657ba3315e Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Tue, 11 Nov 2025 12:51:44 -0800 Subject: [PATCH 57/62] remove import --- tests/unit_tests/extrinsics/test_proxy.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/unit_tests/extrinsics/test_proxy.py b/tests/unit_tests/extrinsics/test_proxy.py index 2bff4fcb21..c0bc4bc6b7 100644 --- a/tests/unit_tests/extrinsics/test_proxy.py +++ b/tests/unit_tests/extrinsics/test_proxy.py @@ -1,9 +1,8 @@ -import pytest +from bittensor_wallet import Wallet +from scalecodec.types import GenericCall from bittensor.core.extrinsics import proxy from bittensor.core.types import ExtrinsicResponse -from scalecodec.types import GenericCall -from bittensor_wallet import Wallet def test_add_proxy_extrinsic(subtensor, mocker): From d6ef42d965de36371d6a065b7301110d6fcd823b Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Tue, 11 Nov 2025 13:06:13 -0800 Subject: [PATCH 58/62] add unit tests for async extrinsics --- .../extrinsics/asyncex/test_proxy.py | 623 ++++++++++++++++++ 1 file changed, 623 insertions(+) create mode 100644 tests/unit_tests/extrinsics/asyncex/test_proxy.py diff --git a/tests/unit_tests/extrinsics/asyncex/test_proxy.py b/tests/unit_tests/extrinsics/asyncex/test_proxy.py new file mode 100644 index 0000000000..5076380210 --- /dev/null +++ b/tests/unit_tests/extrinsics/asyncex/test_proxy.py @@ -0,0 +1,623 @@ +import pytest + +from bittensor.core.extrinsics.asyncex import proxy +from bittensor.core.types import ExtrinsicResponse +from scalecodec.types import GenericCall +from bittensor_wallet import Wallet + + +@pytest.mark.asyncio +async def test_add_proxy_extrinsic(subtensor, mocker): + """Verify that sync `add_proxy_extrinsic` method calls proper async method.""" + # Preps + wallet = mocker.MagicMock(spec=Wallet) + delegate_ss58 = mocker.MagicMock(spec=str) + proxy_type = mocker.MagicMock(spec=proxy.ProxyType) + delay = mocker.MagicMock(spec=int) + + mocked_normalize = mocker.patch.object(proxy.ProxyType, "normalize") + mocked_pallet_call = mocker.patch.object( + proxy.Proxy, "add_proxy", new=mocker.AsyncMock() + ) + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # Call + response = await proxy.add_proxy_extrinsic( + subtensor=subtensor, + wallet=wallet, + delegate_ss58=delegate_ss58, + proxy_type=proxy_type, + delay=delay, + ) + + # Asserts + mocked_normalize.assert_called_once_with(proxy_type) + mocked_pallet_call.assert_awaited_once_with( + delegate=delegate_ss58, + proxy_type=mocked_normalize.return_value, + delay=delay, + ) + mocked_sign_and_send_extrinsic.assert_awaited_once_with( + call=mocked_pallet_call.return_value, + wallet=wallet, + raise_error=False, + period=None, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_sign_and_send_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_remove_proxy_extrinsic(subtensor, mocker): + """Verify that sync `remove_proxy_extrinsic` method calls proper methods.""" + # Preps + wallet = mocker.MagicMock(spec=Wallet) + delegate_ss58 = mocker.MagicMock(spec=str) + proxy_type = mocker.MagicMock(spec=proxy.ProxyType) + delay = mocker.MagicMock(spec=int) + + mocked_normalize = mocker.patch.object(proxy.ProxyType, "normalize") + mocked_pallet_call = mocker.patch.object( + proxy.Proxy, "remove_proxy", new=mocker.AsyncMock() + ) + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # Call + response = await proxy.remove_proxy_extrinsic( + subtensor=subtensor, + wallet=wallet, + delegate_ss58=delegate_ss58, + proxy_type=proxy_type, + delay=delay, + ) + + # Asserts + mocked_normalize.assert_called_once_with(proxy_type) + mocked_pallet_call.assert_awaited_once_with( + delegate=delegate_ss58, + proxy_type=mocked_normalize.return_value, + delay=delay, + ) + mocked_sign_and_send_extrinsic.assert_awaited_once_with( + call=mocked_pallet_call.return_value, + wallet=wallet, + raise_error=False, + period=None, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_sign_and_send_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_remove_proxies_extrinsic(subtensor, mocker): + """Verify that sync `remove_proxies_extrinsic` method calls proper methods.""" + # Preps + wallet = mocker.MagicMock(spec=Wallet) + + mocked_pallet_call = mocker.patch.object( + proxy.Proxy, "remove_proxies", new=mocker.AsyncMock() + ) + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # Call + response = await proxy.remove_proxies_extrinsic( + subtensor=subtensor, + wallet=wallet, + ) + + # Asserts + mocked_pallet_call.assert_awaited_once() + mocked_sign_and_send_extrinsic.assert_awaited_once_with( + call=mocked_pallet_call.return_value, + wallet=wallet, + raise_error=False, + period=None, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_sign_and_send_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_create_pure_proxy_extrinsic(subtensor, mocker): + """Verify that sync `create_pure_proxy_extrinsic` method calls proper methods.""" + # Preps + wallet = mocker.MagicMock(spec=Wallet) + proxy_type = mocker.MagicMock(spec=proxy.ProxyType) + delay = mocker.MagicMock(spec=int) + index = mocker.MagicMock(spec=int) + + mocked_normalize = mocker.patch.object(proxy.ProxyType, "normalize") + mocked_pallet_call = mocker.patch.object( + proxy.Proxy, "create_pure", new=mocker.AsyncMock() + ) + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # Mock response with events + mock_response = mocker.MagicMock(spec=ExtrinsicResponse) + mock_response.success = True + mock_response.extrinsic_receipt = mocker.MagicMock() + mock_response.extrinsic_receipt.triggered_events = mocker.AsyncMock( + return_value=[ + { + "event_id": "PureCreated", + "attributes": { + "pure": "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty", + "who": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", + "proxy_type": "Any", + "disambiguation_index": 0, + }, + } + ] + )() + mock_response.extrinsic_receipt.block_hash = mocker.AsyncMock(spec=str) + mock_response.extrinsic_receipt.extrinsic_idx = mocker.AsyncMock(return_value=1)() + mocked_sign_and_send_extrinsic.return_value = mock_response + + mocked_get_block_number = mocker.patch.object( + subtensor.substrate, "get_block_number", new=mocker.AsyncMock() + ) + + # Call + response = await proxy.create_pure_proxy_extrinsic( + subtensor=subtensor, + wallet=wallet, + proxy_type=proxy_type, + delay=delay, + index=index, + ) + + # Asserts + mocked_normalize.assert_called_once_with(proxy_type) + mocked_pallet_call.assert_awaited_once_with( + proxy_type=mocked_normalize.return_value, + delay=delay, + index=index, + ) + mocked_sign_and_send_extrinsic.assert_awaited_once_with( + call=mocked_pallet_call.return_value, + wallet=wallet, + raise_error=False, + period=None, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + mocked_get_block_number.assert_called_once() + assert response == mock_response + assert ( + response.data["pure_account"] + == "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + ) + assert ( + response.data["spawner"] == "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" + ) + assert response.data["height"] == mocked_get_block_number.return_value + assert response.data["ext_index"] == 1 + + +@pytest.mark.asyncio +async def test_kill_pure_proxy_extrinsic(subtensor, mocker): + """Verify that sync `kill_pure_proxy_extrinsic` method calls proper methods.""" + # Preps + wallet = mocker.MagicMock(spec=Wallet) + wallet.coldkey.ss58_address = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" + pure_proxy_ss58 = mocker.MagicMock(spec=str) + spawner = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" + proxy_type = mocker.MagicMock(spec=proxy.ProxyType) + index = mocker.MagicMock(spec=int) + height = mocker.MagicMock(spec=int) + ext_index = mocker.MagicMock(spec=int) + + mocked_normalize = mocker.patch.object(proxy.ProxyType, "normalize") + mocked_pallet_call = mocker.patch.object( + proxy.Proxy, "kill_pure", new=mocker.AsyncMock() + ) + mocked_proxy_extrinsic = mocker.patch.object( + proxy, "proxy_extrinsic", new=mocker.AsyncMock() + ) + + # Call + response = await proxy.kill_pure_proxy_extrinsic( + subtensor=subtensor, + wallet=wallet, + pure_proxy_ss58=pure_proxy_ss58, + spawner=spawner, + proxy_type=proxy_type, + index=index, + height=height, + ext_index=ext_index, + ) + + # Asserts + mocked_normalize.assert_called_once_with(proxy_type) + mocked_pallet_call.assert_awaited_once_with( + spawner=spawner, + proxy_type=mocked_normalize.return_value, + index=index, + height=height, + ext_index=ext_index, + ) + mocked_proxy_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + wallet=wallet, + real_account_ss58=pure_proxy_ss58, + force_proxy_type=proxy.ProxyType.Any, + call=mocked_pallet_call.return_value, + period=None, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_proxy_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_kill_pure_proxy_extrinsic_spawner_mismatch(subtensor, mocker): + """Verify that `kill_pure_proxy_extrinsic` returns error when spawner doesn't match wallet.""" + # Preps + wallet = mocker.MagicMock(spec=Wallet) + wallet.coldkey.ss58_address = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + pure_proxy_ss58 = mocker.MagicMock(spec=str) + spawner = ( + "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" # Different from wallet + ) + proxy_type = mocker.MagicMock(spec=proxy.ProxyType) + index = mocker.MagicMock(spec=int) + height = mocker.MagicMock(spec=int) + ext_index = mocker.MagicMock(spec=int) + + mocked_normalize = mocker.patch.object(proxy.ProxyType, "normalize") + + # Call + response = await proxy.kill_pure_proxy_extrinsic( + subtensor=subtensor, + wallet=wallet, + pure_proxy_ss58=pure_proxy_ss58, + spawner=spawner, + proxy_type=proxy_type, + index=index, + height=height, + ext_index=ext_index, + ) + + # Asserts + mocked_normalize.assert_called_once_with(proxy_type) + assert response.success is False + assert "Spawner address" in response.message + + +@pytest.mark.asyncio +async def test_proxy_extrinsic(subtensor, mocker): + """Verify that sync `proxy_extrinsic` method calls proper methods.""" + # Preps + wallet = mocker.MagicMock(spec=Wallet) + real_account_ss58 = mocker.MagicMock(spec=str) + force_proxy_type = mocker.MagicMock(spec=str) + call = mocker.MagicMock(spec=GenericCall) + + mocked_normalize = mocker.patch.object(proxy.ProxyType, "normalize") + mocked_pallet_call = mocker.patch.object( + proxy.Proxy, "proxy", new=mocker.AsyncMock() + ) + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # Call + response = await proxy.proxy_extrinsic( + subtensor=subtensor, + wallet=wallet, + real_account_ss58=real_account_ss58, + force_proxy_type=force_proxy_type, + call=call, + ) + + # Asserts + mocked_normalize.assert_called_once_with(force_proxy_type) + mocked_pallet_call.assert_awaited_once_with( + real=real_account_ss58, + force_proxy_type=mocked_normalize.return_value, + call=call, + ) + mocked_sign_and_send_extrinsic.assert_awaited_once_with( + call=mocked_pallet_call.return_value, + wallet=wallet, + raise_error=False, + period=None, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_sign_and_send_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_proxy_extrinsic_with_none_force_proxy_type(subtensor, mocker): + """Verify that sync `proxy_extrinsic` method handles None force_proxy_type.""" + # Preps + wallet = mocker.MagicMock(spec=Wallet) + real_account_ss58 = mocker.MagicMock(spec=str) + force_proxy_type = None + call = mocker.MagicMock(spec=GenericCall) + + mocked_pallet_call = mocker.patch.object( + proxy.Proxy, "proxy", new=mocker.AsyncMock() + ) + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # Call + response = await proxy.proxy_extrinsic( + subtensor=subtensor, + wallet=wallet, + real_account_ss58=real_account_ss58, + force_proxy_type=force_proxy_type, + call=call, + ) + + # Asserts + mocked_pallet_call.assert_awaited_once_with( + real=real_account_ss58, + force_proxy_type=None, + call=call, + ) + mocked_sign_and_send_extrinsic.assert_awaited_once_with( + call=mocked_pallet_call.return_value, + wallet=wallet, + raise_error=False, + period=None, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_sign_and_send_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_proxy_announced_extrinsic(subtensor, mocker): + """Verify that sync `proxy_announced_extrinsic` method calls proper methods.""" + # Preps + wallet = mocker.MagicMock(spec=Wallet) + delegate_ss58 = mocker.MagicMock(spec=str) + real_account_ss58 = mocker.MagicMock(spec=str) + force_proxy_type = mocker.MagicMock(spec=str) + call = mocker.MagicMock(spec=GenericCall) + + mocked_normalize = mocker.patch.object(proxy.ProxyType, "normalize") + mocked_pallet_call = mocker.patch.object( + proxy.Proxy, "proxy_announced", new=mocker.AsyncMock() + ) + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # Call + response = await proxy.proxy_announced_extrinsic( + subtensor=subtensor, + wallet=wallet, + delegate_ss58=delegate_ss58, + real_account_ss58=real_account_ss58, + force_proxy_type=force_proxy_type, + call=call, + ) + + # Asserts + mocked_normalize.assert_called_once_with(force_proxy_type) + mocked_pallet_call.assert_awaited_once_with( + delegate=delegate_ss58, + real=real_account_ss58, + force_proxy_type=mocked_normalize.return_value, + call=call, + ) + mocked_sign_and_send_extrinsic.assert_awaited_once_with( + call=mocked_pallet_call.return_value, + wallet=wallet, + raise_error=False, + period=None, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_sign_and_send_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_proxy_announced_extrinsic_with_none_force_proxy_type(subtensor, mocker): + """Verify that sync `proxy_announced_extrinsic` method handles None force_proxy_type.""" + # Preps + wallet = mocker.MagicMock(spec=Wallet) + delegate_ss58 = mocker.MagicMock(spec=str) + real_account_ss58 = mocker.MagicMock(spec=str) + force_proxy_type = None + call = mocker.MagicMock(spec=GenericCall) + + mocked_pallet_call = mocker.patch.object( + proxy.Proxy, "proxy_announced", new=mocker.AsyncMock() + ) + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # Call + response = await proxy.proxy_announced_extrinsic( + subtensor=subtensor, + wallet=wallet, + delegate_ss58=delegate_ss58, + real_account_ss58=real_account_ss58, + force_proxy_type=force_proxy_type, + call=call, + ) + + # Asserts + mocked_pallet_call.assert_awaited_once_with( + delegate=delegate_ss58, + real=real_account_ss58, + force_proxy_type=None, + call=call, + ) + mocked_sign_and_send_extrinsic.assert_awaited_once_with( + call=mocked_pallet_call.return_value, + wallet=wallet, + raise_error=False, + period=None, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_sign_and_send_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_announce_extrinsic(subtensor, mocker): + """Verify that sync `announce_extrinsic` method calls proper methods.""" + # Preps + wallet = mocker.MagicMock(spec=Wallet) + real_account_ss58 = mocker.MagicMock(spec=str) + call_hash = mocker.MagicMock(spec=str) + + mocked_pallet_call = mocker.patch.object( + proxy.Proxy, "announce", new=mocker.AsyncMock() + ) + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # Call + response = await proxy.announce_extrinsic( + subtensor=subtensor, + wallet=wallet, + real_account_ss58=real_account_ss58, + call_hash=call_hash, + ) + + # Asserts + mocked_pallet_call.assert_awaited_once_with( + real=real_account_ss58, + call_hash=call_hash.lstrip().__radd__(), + ) + mocked_sign_and_send_extrinsic.assert_awaited_once_with( + call=mocked_pallet_call.return_value, + wallet=wallet, + raise_error=False, + period=None, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_sign_and_send_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_reject_announcement_extrinsic(subtensor, mocker): + """Verify that sync `reject_announcement_extrinsic` method calls proper methods.""" + # Preps + wallet = mocker.MagicMock(spec=Wallet) + delegate_ss58 = mocker.MagicMock(spec=str) + call_hash = mocker.MagicMock(spec=str) + + mocked_pallet_call = mocker.patch.object( + proxy.Proxy, "reject_announcement", new=mocker.AsyncMock() + ) + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # Call + response = await proxy.reject_announcement_extrinsic( + subtensor=subtensor, + wallet=wallet, + delegate_ss58=delegate_ss58, + call_hash=call_hash, + ) + + # Asserts + mocked_pallet_call.assert_awaited_once_with( + delegate=delegate_ss58, + call_hash=call_hash.lstrip().__radd__(), + ) + mocked_sign_and_send_extrinsic.assert_awaited_once_with( + call=mocked_pallet_call.return_value, + wallet=wallet, + raise_error=False, + period=None, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_sign_and_send_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_remove_announcement_extrinsic(subtensor, mocker): + """Verify that sync `remove_announcement_extrinsic` method calls proper methods.""" + # Preps + wallet = mocker.MagicMock(spec=Wallet) + real_account_ss58 = mocker.MagicMock(spec=str) + call_hash = mocker.MagicMock(spec=str) + + mocked_pallet_call = mocker.patch.object( + proxy.Proxy, "remove_announcement", new=mocker.AsyncMock() + ) + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # Call + response = await proxy.remove_announcement_extrinsic( + subtensor=subtensor, + wallet=wallet, + real_account_ss58=real_account_ss58, + call_hash=call_hash, + ) + + # Asserts + mocked_pallet_call.assert_awaited_once_with( + real=real_account_ss58, + call_hash=call_hash.lstrip().__radd__(), + ) + mocked_sign_and_send_extrinsic.assert_awaited_once_with( + call=mocked_pallet_call.return_value, + wallet=wallet, + raise_error=False, + period=None, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_sign_and_send_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_poke_deposit_extrinsic(subtensor, mocker): + """Verify that sync `poke_deposit_extrinsic` method calls proper methods.""" + # Preps + wallet = mocker.MagicMock(spec=Wallet) + + mocked_pallet_call = mocker.patch.object( + proxy.Proxy, "poke_deposit", new=mocker.AsyncMock() + ) + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic" + ) + + # Call + response = await proxy.poke_deposit_extrinsic( + subtensor=subtensor, + wallet=wallet, + ) + + # Asserts + mocked_pallet_call.assert_awaited_once() + mocked_sign_and_send_extrinsic.assert_awaited_once_with( + call=mocked_pallet_call.return_value, + wallet=wallet, + raise_error=False, + period=None, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert response == mocked_sign_and_send_extrinsic.return_value From 35eda5d7f37f54d2d18e2b982af1e92673cd64d9 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Tue, 11 Nov 2025 13:06:34 -0800 Subject: [PATCH 59/62] improve `kill_pure_proxy` subtensor methods --- bittensor/core/async_subtensor.py | 9 +++++++++ bittensor/core/subtensor.py | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 4c81fafbbb..e9c8b8796f 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -6007,6 +6007,7 @@ async def kill_pure_proxy( index: int, height: int, ext_index: int, + force_proxy_type: Optional[Union[str, "ProxyType"]] = ProxyType.Any, period: Optional[int] = DEFAULT_PERIOD, raise_error: bool = False, wait_for_inclusion: bool = True, @@ -6032,6 +6033,13 @@ async def kill_pure_proxy( index: The disambiguation index originally passed to `create_pure()`. height: The block height at which the pure proxy was created. ext_index: The extrinsic index at which the pure proxy was created. + force_proxy_type: The proxy type relationship to use when executing `kill_pure` through the proxy mechanism. + Since pure proxies are keyless and cannot sign transactions, the spawner must act as a proxy for the + pure proxy to execute `kill_pure`. This parameter specifies which proxy type relationship between the + spawner and the pure proxy account should be used. The spawner must have a proxy relationship of this + type (or `Any`) with the pure proxy account. Defaults to `ProxyType.Any` for maximum compatibility. If + `None`, Substrate will automatically select an available proxy type from the spawner's proxy + relationships. 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. @@ -6056,6 +6064,7 @@ async def kill_pure_proxy( index=index, height=height, ext_index=ext_index, + force_proxy_type=force_proxy_type, period=period, raise_error=raise_error, wait_for_inclusion=wait_for_inclusion, diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index e2d80280bb..d1e4caa9f6 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -4715,6 +4715,7 @@ def kill_pure_proxy( index: int, height: int, ext_index: int, + force_proxy_type: Optional[Union[str, "ProxyType"]] = ProxyType.Any, period: Optional[int] = DEFAULT_PERIOD, raise_error: bool = False, wait_for_inclusion: bool = True, @@ -4740,6 +4741,13 @@ def kill_pure_proxy( index: The disambiguation index originally passed to `create_pure`. height: The block height at which the pure proxy was created. ext_index: The extrinsic index at which the pure proxy was created. + force_proxy_type: The proxy type relationship to use when executing `kill_pure` through the proxy mechanism. + Since pure proxies are keyless and cannot sign transactions, the spawner must act as a proxy for the + pure proxy to execute `kill_pure`. This parameter specifies which proxy type relationship between the + spawner and the pure proxy account should be used. The spawner must have a proxy relationship of this + type (or `Any`) with the pure proxy account. Defaults to `ProxyType.Any` for maximum compatibility. If + `None`, Substrate will automatically select an available proxy type from the spawner's proxy + relationships. 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. @@ -4764,6 +4772,7 @@ def kill_pure_proxy( index=index, height=height, ext_index=ext_index, + force_proxy_type=force_proxy_type, period=period, raise_error=raise_error, wait_for_inclusion=wait_for_inclusion, From e7de598a891d9b5f041988ae34a26e79e84d5719 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Tue, 11 Nov 2025 13:12:55 -0800 Subject: [PATCH 60/62] update unit tests `test_kill_pure_proxy` --- tests/unit_tests/test_async_subtensor.py | 1 + tests/unit_tests/test_subtensor.py | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 6b90acd684..ecff158286 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -5320,6 +5320,7 @@ async def test_kill_pure_proxy(mocker, subtensor): index=index, height=height, ext_index=ext_index, + force_proxy_type=async_subtensor.ProxyType.Any, period=DEFAULT_PERIOD, raise_error=False, wait_for_inclusion=True, diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 47df324f5f..c51e654e11 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -5393,6 +5393,7 @@ def test_kill_pure_proxy(mocker, subtensor): index=index, height=height, ext_index=ext_index, + force_proxy_type=subtensor_module.ProxyType.Any, period=DEFAULT_PERIOD, raise_error=False, wait_for_inclusion=True, From 7774ab21bb04bd77fe077c42bc2b5ec97dd96dbf Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Tue, 11 Nov 2025 13:33:21 -0800 Subject: [PATCH 61/62] add warning to `kill_pure_proxy` and `kill_pure_proxy_extrinsic` --- bittensor/core/async_subtensor.py | 3 +++ bittensor/core/extrinsics/asyncex/proxy.py | 3 +++ bittensor/core/extrinsics/proxy.py | 3 +++ bittensor/core/subtensor.py | 3 +++ 4 files changed, 12 insertions(+) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index e9c8b8796f..9b3d3fd936 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -6054,6 +6054,9 @@ async def kill_pure_proxy( The `kill_pure` call must be executed through the pure proxy account itself, with the spawner acting as an "Any" proxy. This method automatically handles this by executing the call via `proxy()`. The spawner must have an "Any" proxy relationship with the pure proxy for this to work. + + Warning: + Any funds remaining in the pure proxy account will become permanently inaccessible after this operation. """ return await kill_pure_proxy_extrinsic( subtensor=self, diff --git a/bittensor/core/extrinsics/asyncex/proxy.py b/bittensor/core/extrinsics/asyncex/proxy.py index 84b52262fa..3fe84ffa9e 100644 --- a/bittensor/core/extrinsics/asyncex/proxy.py +++ b/bittensor/core/extrinsics/asyncex/proxy.py @@ -358,6 +358,9 @@ async def kill_pure_proxy_extrinsic( pass a different `force_proxy_type`, the spawner must have that specific proxy type relationship with the pure proxy. + Warning: + Any funds remaining in the pure proxy account will become permanently inaccessible after this operation. + Example: # After creating a pure proxy create_response = subtensor.proxies.create_pure_proxy( diff --git a/bittensor/core/extrinsics/proxy.py b/bittensor/core/extrinsics/proxy.py index cb3c9736cc..ece58758da 100644 --- a/bittensor/core/extrinsics/proxy.py +++ b/bittensor/core/extrinsics/proxy.py @@ -357,6 +357,9 @@ def kill_pure_proxy_extrinsic( pass a different `force_proxy_type`, the spawner must have that specific proxy type relationship with the pure proxy. + Warning: + Any funds remaining in the pure proxy account will become permanently inaccessible after this operation. + Example: # After creating a pure proxy create_response = subtensor.proxies.create_pure_proxy( diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index d1e4caa9f6..3ae898550e 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -4762,6 +4762,9 @@ def kill_pure_proxy( The `kill_pure` call must be executed through the pure proxy account itself, with the spawner acting as an "Any" proxy. This method automatically handles this by executing the call via `proxy()`. The spawner must have an "Any" proxy relationship with the pure proxy for this to work. + + Warning: + Any funds remaining in the pure proxy account will become permanently inaccessible after this operation. """ return kill_pure_proxy_extrinsic( subtensor=self, From 55c64a2d972b3375fb542bf04baaa64977c7e6c2 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Wed, 12 Nov 2025 13:06:44 -0800 Subject: [PATCH 62/62] update Warnings --- bittensor/core/async_subtensor.py | 3 ++- bittensor/core/extrinsics/asyncex/proxy.py | 3 ++- bittensor/core/extrinsics/proxy.py | 3 ++- bittensor/core/subtensor.py | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 9b3d3fd936..f332104646 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -6056,7 +6056,8 @@ async def kill_pure_proxy( have an "Any" proxy relationship with the pure proxy for this to work. Warning: - Any funds remaining in the pure proxy account will become permanently inaccessible after this operation. + All access to this account will be lost. Any funds remaining in the pure proxy account will become + permanently inaccessible after this operation. """ return await kill_pure_proxy_extrinsic( subtensor=self, diff --git a/bittensor/core/extrinsics/asyncex/proxy.py b/bittensor/core/extrinsics/asyncex/proxy.py index 3fe84ffa9e..194b4b8f16 100644 --- a/bittensor/core/extrinsics/asyncex/proxy.py +++ b/bittensor/core/extrinsics/asyncex/proxy.py @@ -359,7 +359,8 @@ async def kill_pure_proxy_extrinsic( proxy. Warning: - Any funds remaining in the pure proxy account will become permanently inaccessible after this operation. + All access to this account will be lost. Any funds remaining in the pure proxy account will become permanently + inaccessible after this operation. Example: # After creating a pure proxy diff --git a/bittensor/core/extrinsics/proxy.py b/bittensor/core/extrinsics/proxy.py index ece58758da..f20808abeb 100644 --- a/bittensor/core/extrinsics/proxy.py +++ b/bittensor/core/extrinsics/proxy.py @@ -358,7 +358,8 @@ def kill_pure_proxy_extrinsic( proxy. Warning: - Any funds remaining in the pure proxy account will become permanently inaccessible after this operation. + All access to this account will be lost. Any funds remaining in the pure proxy account will become permanently + inaccessible after this operation. Example: # After creating a pure proxy diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 3ae898550e..7c1c9ce9c4 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -4764,7 +4764,8 @@ def kill_pure_proxy( have an "Any" proxy relationship with the pure proxy for this to work. Warning: - Any funds remaining in the pure proxy account will become permanently inaccessible after this operation. + All access to this account will be lost. Any funds remaining in the pure proxy account will become + permanently inaccessible after this operation. """ return kill_pure_proxy_extrinsic( subtensor=self,