diff --git a/.github/workflows/subtensor-consistency-tests.yaml b/.github/workflows/subtensor-consistency-tests.yaml new file mode 100644 index 0000000000..b7e02abf4d --- /dev/null +++ b/.github/workflows/subtensor-consistency-tests.yaml @@ -0,0 +1,221 @@ +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.label }}" + 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 + + - 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.nodeid }} -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/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 914df7475b..f332104646 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,212 @@ 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 = {} + 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 = {} + 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, @@ -5297,6 +5520,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", @@ -5550,6 +5869,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", @@ -5629,6 +5998,83 @@ 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, + force_proxy_type: Optional[Union[str, "ProxyType"]] = ProxyType.Any, + 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. + 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. + 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. + + Warning: + 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, + wallet=wallet, + pure_proxy_ss58=pure_proxy_ss58, + spawner=spawner, + proxy_type=proxy_type, + 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, + wait_for_finalization=wait_for_finalization, + ) + async def modify_liquidity( self, wallet: "Wallet", @@ -5752,6 +6198,152 @@ 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. + + 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, + 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", @@ -5796,6 +6388,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", @@ -5894,6 +6531,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", @@ -5939,6 +6622,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", diff --git a/bittensor/core/chain_data/__init__.py b/bittensor/core/chain_data/__init__.py index a232d8b651..b8d00b75fb 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, ProxyType, ProxyAnnouncementInfo from .scheduled_coldkey_swap_info import ScheduledColdkeySwapInfo from .stake_info import StakeInfo from .sim_swap import SimSwapResult @@ -54,6 +55,10 @@ "PrometheusInfo", "ProposalCallData", "ProposalVoteData", + "ProxyConstants", + "ProxyAnnouncementInfo", + "ProxyInfo", + "ProxyType", "ScheduledColdkeySwapInfo", "SelectiveMetagraphIndex", "SimSwapResult", diff --git a/bittensor/core/chain_data/proxy.py b/bittensor/core/chain_data/proxy.py new file mode 100644 index 0000000000..f8735f6e95 --- /dev/null +++ b/bittensor/core/chain_data/proxy.py @@ -0,0 +1,352 @@ +from dataclasses import dataclass +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. + + 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" + Owner = "Owner" + NonCritical = "NonCritical" + NonTransfer = "NonTransfer" + NonFungible = "NonFungible" + Staking = "Staking" + Registration = "Registration" + Transfer = "Transfer" + SmallTransfer = "SmallTransfer" + ChildKeys = "ChildKeys" + SudoUncheckedSetCode = "SudoUncheckedSetCode" + SwapHotkey = "SwapHotkey" + 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.""" + 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: + """ + 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", "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_tuple(cls, data: tuple) -> list["ProxyInfo"]: + """Returns a list of ProxyInfo objects from a tuple of proxy data. + + Parameters: + data: Tuple of chain proxy data. + + Returns: + List of ProxyInfo objects. + """ + return [ + cls( + delegate=decode_account_id(proxy["delegate"]), + proxy_type=next(iter(proxy["proxy_type"].keys())), + delay=proxy["delay"], + ) + for proxy in data + ] + + @classmethod + 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 `query()` call to `Proxy.Proxies` storage function. + + 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) + + @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 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 or None if no announcements aren't found. + """ + 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 +class ProxyConstants: + """ + Represents all runtime constants defined in the `Proxy` pallet. + + 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. + """ + + 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) diff --git a/bittensor/core/extrinsics/asyncex/proxy.py b/bittensor/core/extrinsics/asyncex/proxy.py new file mode 100644 index 0000000000..194b4b8f16 --- /dev/null +++ b/bittensor/core/extrinsics/asyncex/proxy.py @@ -0,0 +1,880 @@ +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 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", []) + 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": await 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, + force_proxy_type: Optional[Union[str, ProxyType]] = ProxyType.Any, + 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 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. + 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 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. + + Warning: + 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 + create_response = subtensor.proxies.create_pure_proxy( + wallet=spawner_wallet, + 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=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: + 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 (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=force_proxy_type, + 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 + + # 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] " + 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 + + # 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] " + 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 + + # 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] " + 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. + + 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 ( + 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) 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 new file mode 100644 index 0000000000..fbb73c441b --- /dev/null +++ b/bittensor/core/extrinsics/pallets/proxy.py @@ -0,0 +1,244 @@ +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"). + 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. + 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 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, + 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. + 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_type: str, + index: int, + 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_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. + + Returns: + GenericCall instance. + """ + return self.create_composed_call( + spawner=spawner, + proxy_type=proxy_type, + index=index, + 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. + 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, + ) + + 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, + ) + + def poke_deposit(self) -> Call: + """Returns GenericCall instance for Proxy.poke_deposit. + + Returns: + GenericCall instance. + """ + return self.create_composed_call() diff --git a/bittensor/core/extrinsics/proxy.py b/bittensor/core/extrinsics/proxy.py new file mode 100644 index 0000000000..f20808abeb --- /dev/null +++ b/bittensor/core/extrinsics/proxy.py @@ -0,0 +1,879 @@ +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 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 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) + + +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", + 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]") + + # 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]") + + 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", + pure_proxy_ss58: str, + spawner: str, + proxy_type: Union[str, "ProxyType"], + 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, + 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 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. + 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 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. + + Warning: + 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 + create_response = subtensor.proxies.create_pure_proxy( + wallet=spawner_wallet, + 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=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: + 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 = 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 (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=force_proxy_type, + 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) + + +def proxy_extrinsic( + subtensor: "Subtensor", + 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 = Proxy(subtensor).proxy( + real=real_account_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 proxy_announced_extrinsic( + subtensor: "Subtensor", + 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 = Proxy(subtensor).proxy_announced( + delegate=delegate_ss58, + real=real_account_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_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 + + # 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] " + f"on [blue]{subtensor.network}[/blue]." + ) + + call = Proxy(subtensor).announce( + real=real_account_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 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 + + # 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] " + 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_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 + + # 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] " + f"on [blue]{subtensor.network}[/blue]." + ) + + call = Proxy(subtensor).remove_announcement( + real=real_account_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]") + + return response + + 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. + + 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 ( + 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) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index d658675ed3..7c1c9ce9c4 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,11 @@ MetagraphInfo, NeuronInfo, NeuronInfoLite, + ProposalVoteData, + ProxyAnnouncementInfo, + ProxyInfo, + ProxyConstants, + ProxyType, SelectiveMetagraphIndex, SimSwapResult, StakeInfo, @@ -67,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, @@ -2223,6 +2240,179 @@ def get_parents( return [] + 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 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. + + 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. + """ + block_hash = self.determine_block_hash(block) + query = self.substrate.query( + module="Proxy", + storage_function="Proxies", + params=[real_account_ss58], + block_hash=block_hash, + ) + return ProxyInfo.from_query(query) + + def get_proxy_announcement( + self, + delegate_account_ss58: str, + block: Optional[int] = None, + ) -> 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. + + 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, + constants: Optional[list[str]] = None, + as_dict: bool = False, + block: Optional[int] = None, + ) -> 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. + + 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() + + 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, @@ -4041,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", @@ -4291,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", @@ -4370,6 +4706,83 @@ def finalize_crowdloan( wait_for_finalization=wait_for_finalization, ) + 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, + force_proxy_type: Optional[Union[str, "ProxyType"]] = ProxyType.Any, + 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. + 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. + 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. + + Warning: + 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, + wallet=wallet, + pure_proxy_ss58=pure_proxy_ss58, + spawner=spawner, + proxy_type=proxy_type, + 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, + wait_for_finalization=wait_for_finalization, + ) + def modify_liquidity( self, wallet: "Wallet", @@ -4493,6 +4906,152 @@ 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. + + 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, + 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", @@ -4537,6 +5096,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", @@ -4635,6 +5239,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", @@ -4680,6 +5330,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", 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 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 diff --git a/tests/consistency/__init__.py b/tests/consistency/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/consistency/conftest.py b/tests/consistency/conftest.py new file mode 100644 index 0000000000..f6029fedc1 --- /dev/null +++ b/tests/consistency/conftest.py @@ -0,0 +1,3 @@ +from ..e2e_tests.conftest import * + +local_chain = local_chain diff --git a/tests/consistency/test_proxy_types.py b/tests/consistency/test_proxy_types.py new file mode 100644 index 0000000000..ab9462f8bc --- /dev/null +++ b/tests/consistency/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()) diff --git a/tests/e2e_tests/test_proxy.py b/tests/e2e_tests/test_proxy.py new file mode 100644 index 0000000000..f4555edcac --- /dev/null +++ b/tests/e2e_tests/test_proxy.py @@ -0,0 +1,1481 @@ +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): + """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 === + 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( + 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 + + 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( + 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) + + +@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 +): + """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 + + +@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 = await async_subtensor.block + delay + + # === Test 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] + 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 = 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." + ] + + # === delay block need to be awaited after announcement === + await async_subtensor.wait_for_block(registration_block) + + # === proxy call - register subnet === + 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 response.success, response.message + assert await async_subtensor.subnets.get_total_subnets() == 3 + + # === Verify announcement is consumed (cannot reuse) === + assert not await async_subtensor.proxies.get_proxy_announcements() + + # === check that subnet is not active === + 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 = await async_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 = await SubtensorModule(async_subtensor).register_network( + hotkey=alice_wallet.hotkey.ss58_address + ) + test_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=test_call_hash, + ) + assert response.success, response.message + + # Real account rejects the 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, + ) + assert response.success, response.message + + # Verify announcement was removed + 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 + 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 = await SubtensorModule(async_subtensor).register_network( + hotkey=alice_wallet.hotkey.ss58_address + ) + test_call_hash2 = "0x" + test_call2.call_hash.hex() + + response = await async_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 = await async_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 = 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 = await async_subtensor.block + delay + await async_subtensor.wait_for_block(activation_block) + + # === proxy call - activate subnet === + 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_activating_call, + ) + assert response.success, response.message + 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 = await async_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 = await SubtensorModule(async_subtensor).register_network( + hotkey=alice_wallet.hotkey.ss58_address + ) + response = await async_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 = await SubtensorModule(async_subtensor).register_network( + hotkey=alice_wallet.hotkey.ss58_address + ) + correct_call_hash = "0x" + correct_call.call_hash.hex() + + response = await async_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 + 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 = 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, + 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. + + 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 = 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 === + # 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, + ) + + 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 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, + ) + 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." + ] + + +@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. + + 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 === + proxies, deposit = subtensor.proxies.get_proxies_for_real_account( + real_account_ss58=real_account_wallet.coldkey.ss58_address + ) + assert not proxies + 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. + + 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 + + +@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 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 diff --git a/tests/unit_tests/extrinsics/test_proxy.py b/tests/unit_tests/extrinsics/test_proxy.py new file mode 100644 index 0000000000..c0bc4bc6b7 --- /dev/null +++ b/tests/unit_tests/extrinsics/test_proxy.py @@ -0,0 +1,578 @@ +from bittensor_wallet import Wallet +from scalecodec.types import GenericCall + +from bittensor.core.extrinsics import proxy +from bittensor.core.types import ExtrinsicResponse + + +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.lstrip().__radd__(), + ) + 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.lstrip().__radd__(), + ) + 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.lstrip().__radd__(), + ) + 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_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index a75413efa8..ecff158286 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -4957,3 +4957,591 @@ 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, + force_proxy_type=async_subtensor.ProxyType.Any, + 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 diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 5dc104ab51..c51e654e11 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -5032,3 +5032,582 @@ 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(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_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( + subtensor_module.ProxyInfo, + "from_query", + ) + + # 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=mocked_determine_block_hash.return_value, + ) + mocked_from_query.assert_called_once_with(mocked_query.return_value) + assert result == mocked_from_query.return_value + + +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( + subtensor_module.ProxyAnnouncementInfo, + "from_dict", + ) + + # 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=mocked_determine_block_hash.return_value, + ) + 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(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 = 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_query_map_records, + ) + + # 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=mocked_determine_block_hash.return_value, + ) + mocked_from_query_map_record.assert_called_once_with(fake_record) + assert result == {fake_delegate: fake_proxies_list} + + +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(subtensor_module.ProxyConstants, "from_dict") + + # Call + result = 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 + + +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( + 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(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_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, + force_proxy_type=subtensor_module.ProxyType.Any, + 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 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