diff --git a/.github/workflows/changelog-checker.yml b/.github/workflows/changelog-checker.yml index 8c8de24a0d..aae9580609 100644 --- a/.github/workflows/changelog-checker.yml +++ b/.github/workflows/changelog-checker.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: tj-actions/changed-files@v42 + - uses: tj-actions/changed-files@v46 id: changed - name: Ensure CHANGELOG.md updated if: contains(steps.changed.outputs.all_changed_files, 'CHANGELOG.md') == false diff --git a/.github/workflows/flake8-and-mypy.yml b/.github/workflows/flake8-and-mypy.yml index 1fbe094728..2e259bdd05 100644 --- a/.github/workflows/flake8-and-mypy.yml +++ b/.github/workflows/flake8-and-mypy.yml @@ -42,7 +42,8 @@ jobs: python -m venv venv source venv/bin/activate python -m pip install --upgrade pip - python -m pip install uv + # needed for Python 3.9 compatibility + python -m pip install uv>=0.8.8 python -m uv sync --extra dev --active - name: Flake8 diff --git a/.github/workflows/nightly-e2e-tests-subtensor-main.yml b/.github/workflows/nightly-e2e-tests-subtensor-main.yml index 589141ec1b..856bf6347b 100644 --- a/.github/workflows/nightly-e2e-tests-subtensor-main.yml +++ b/.github/workflows/nightly-e2e-tests-subtensor-main.yml @@ -76,23 +76,29 @@ jobs: with: name: subtensor-localnet-devnet-ready path: subtensor-localnet-devnet-ready.tar + # Determine the day for non-fast-blocks run - check-if-saturday: + check-if-non-fast-blocks-run: runs-on: ubuntu-latest outputs: - is-saturday: ${{ steps.check.outputs.is-saturday }} + non-fast-blocks-run: ${{ steps.check.outputs.non-fast-blocks-run }} steps: - id: check run: | - day=$(date -u +%u) - echo "Today is weekday $day" - if [ "$day" -ne 6 ]; then - echo "⏭️ Skipping: not Saturday" - echo "is-saturday=false" >> "$GITHUB_OUTPUT" - exit 0 + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "🔁 Manual trigger detected. Forcing non-fast-blocks-run=true" + echo "non-fast-blocks-run=true" >> "$GITHUB_OUTPUT" + else + day=$(date -u +%u) + echo "Today is weekday $day" + if [ "$day" -ne 6 ]; then + echo "⏭️ Skipping: not Saturday" + echo "non-fast-blocks-run=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "✅ It is Saturday" + echo "non-fast-blocks-run=true" >> "$GITHUB_OUTPUT" fi - echo "is-saturday=true" - echo "is-saturday=true" >> "$GITHUB_OUTPUT" # Daily run of fast-blocks tests from `bittensor:master` based on `subtensor:main docker` image run-fast-blocks-e2e-test-master: @@ -102,6 +108,9 @@ jobs: - pull-docker-images 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) @@ -136,22 +145,27 @@ jobs: run: docker load -i subtensor-localnet-main.tar - name: Run tests with retry + id: test-failed env: FAST_BLOCKS: "1" LOCALNET_IMAGE_NAME: "ghcr.io/opentensor/subtensor-localnet:main" run: | set +e for i in 1 2 3; do - echo "🔁 Attempt $i: Running tests" + echo "::group::🔁 Test attempt $i" uv run pytest ${{ matrix.test-file }} -s status=$? if [ $status -eq 0 ]; then echo "✅ Tests passed on attempt $i" + echo "::endgroup::" + echo "failed=false" >> "$GITHUB_OUTPUT" break else echo "❌ Tests failed on attempt $i" + echo "::endgroup::" if [ $i -eq 3 ]; then echo "Tests failed after 3 attempts" + echo "failed=true" >> "$GITHUB_OUTPUT" exit 1 fi echo "Retrying..." @@ -167,6 +181,9 @@ jobs: - pull-docker-images 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) @@ -201,22 +218,27 @@ jobs: run: docker load -i subtensor-localnet-devnet-ready.tar - name: Run tests with retry + id: test-failed env: FAST_BLOCKS: "1" LOCALNET_IMAGE_NAME: "ghcr.io/opentensor/subtensor-localnet:devnet-ready" run: | set +e for i in 1 2 3; do - echo "🔁 Attempt $i: Running tests" + echo "::group::🔁 Test attempt $i" uv run pytest ${{ matrix.test-file }} -s status=$? if [ $status -eq 0 ]; then echo "✅ Tests passed on attempt $i" + echo "::endgroup::" + echo "failed=false" >> "$GITHUB_OUTPUT" break else echo "❌ Tests failed on attempt $i" + echo "::endgroup::" if [ $i -eq 3 ]; then echo "Tests failed after 3 attempts" + echo "failed=true" >> "$GITHUB_OUTPUT" exit 1 fi echo "Retrying..." @@ -226,14 +248,16 @@ jobs: # Saturday run of non-fast-blocks tests from `bittensor:master` based on `subtensor:main` docker image run-non-fast-blocks-e2e-test-master: - if: needs.check-if-saturday.outputs.is-saturday == 'true' + if: needs.check-if-non-fast-blocks-run.outputs.non-fast-blocks-run == 'true' name: "NFB master: ${{ matrix.test-file }} / Python ${{ matrix.python-version }}" needs: - - check-if-saturday + - check-if-non-fast-blocks-run - find-tests - pull-docker-images runs-on: ubuntu-latest timeout-minutes: 1440 + outputs: + failed: ${{ steps.test-failed.outputs.failed }} strategy: fail-fast: false # Allow other matrix jobs to run even if this job fails @@ -270,22 +294,27 @@ jobs: run: docker load -i subtensor-localnet-main.tar - name: Run patched E2E tests + id: test-failed env: FAST_BLOCKS: "0" LOCALNET_IMAGE_NAME: "ghcr.io/opentensor/subtensor-localnet:main" run: | set +e for i in 1 2 3; do - echo "🔁 Attempt $i: Running tests" + echo "::group::🔁 Test attempt $i" uv run pytest ${{ matrix.test-file }} -s status=$? if [ $status -eq 0 ]; then echo "✅ Tests passed on attempt $i" + echo "::endgroup::" + echo "failed=false" >> "$GITHUB_OUTPUT" break else echo "❌ Tests failed on attempt $i" + echo "::endgroup::" if [ $i -eq 3 ]; then echo "Tests failed after 3 attempts" + echo "failed=true" >> "$GITHUB_OUTPUT" exit 1 fi echo "Retrying..." @@ -295,14 +324,16 @@ jobs: # Saturday run of non-fast-blocks tests from `bittensor:staging` based on `subtensor:devnet-ready` docker image run-non-fast-blocks-e2e-test-staging: - if: needs.check-if-saturday.outputs.is-saturday == 'true' + if: needs.check-if-non-fast-blocks-run.outputs.non-fast-blocks-run == 'true' name: "NFB staging: ${{ matrix.test-file }} / Python ${{ matrix.python-version }}" needs: - - check-if-saturday + - check-if-non-fast-blocks-run - find-tests - pull-docker-images runs-on: ubuntu-latest timeout-minutes: 1440 + outputs: + failed: ${{ steps.test-failed.outputs.failed }} strategy: fail-fast: false # Allow other matrix jobs to run even if this job fails @@ -339,25 +370,50 @@ jobs: run: docker load -i subtensor-localnet-devnet-ready.tar - name: Run patched E2E tests + id: test-failed env: FAST_BLOCKS: "0" LOCALNET_IMAGE_NAME: "ghcr.io/opentensor/subtensor-localnet:devnet-ready" run: | set +e for i in 1 2 3; do - echo "🔁 Attempt $i: Running tests" + echo "::group::🔁 Test attempt $i" uv run pytest ${{ matrix.test-file }} -s status=$? if [ $status -eq 0 ]; then echo "✅ Tests passed on attempt $i" + echo "::endgroup::" + echo "failed=false" >> "$GITHUB_OUTPUT" break else echo "❌ Tests failed on attempt $i" + echo "::endgroup::" if [ $i -eq 3 ]; then echo "Tests failed after 3 attempts" + echo "failed=true" >> "$GITHUB_OUTPUT" exit 1 fi echo "Retrying..." sleep 5 fi done + + # Send centralized Discord failure notification + notify-on-failure: + needs: + - run-fast-blocks-e2e-test-master + - run-fast-blocks-e2e-test-staging + - run-non-fast-blocks-e2e-test-master + - run-non-fast-blocks-e2e-test-staging + if: | + needs.run-fast-blocks-e2e-test-master.outputs.failed == 'true' || + needs.run-fast-blocks-e2e-test-staging.outputs.failed == 'true' || + needs.run-non-fast-blocks-e2e-test-master.outputs.failed == 'true' || + needs.run-non-fast-blocks-e2e-test-staging.outputs.failed == 'true' + runs-on: ubuntu-latest + steps: + - name: Send centralized Discord failure notification + run: | + curl -X POST -H "Content-Type: application/json" \ + -d "{\"username\": \"Nightly CI\", \"content\": \"❌ Nightly E2E tests failed. Check run: <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>\"}" \ + "${{ secrets.NIGHTLY_WEBHOOK_URL }}" diff --git a/.github/workflows/unit-and-integration-tests.yml b/.github/workflows/unit-and-integration-tests.yml index 7bc70ae030..ec1fc6ded0 100644 --- a/.github/workflows/unit-and-integration-tests.yml +++ b/.github/workflows/unit-and-integration-tests.yml @@ -39,7 +39,8 @@ jobs: python -m venv venv source venv/bin/activate python -m pip install --upgrade pip - python -m pip install uv + # needed for Python 3.9 compatibility + python -m pip install uv>=0.8.8 python -m uv sync --extra dev --active - name: Unit tests diff --git a/CHANGELOG.md b/CHANGELOG.md index acc5c17f16..1201b090a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # Changelog +## 9.9.0 /2025-08-11 + +## What's Changed +* Fix crv3 tests after devnet-ready get `CRV3WeightCommitsV2` by @basfroman in https://github.com/opentensor/bittensor/pull/2978 +* Add webhook for failed nightly tests by @basfroman in https://github.com/opentensor/bittensor/pull/2987 +* Fix liquidity test (non-fast-blocks node) by @basfroman in https://github.com/opentensor/bittensor/pull/2988 +* improve nightly logic by @basfroman in https://github.com/opentensor/bittensor/pull/2989 +* improve nightly 2 by @basfroman in https://github.com/opentensor/bittensor/pull/2990 +* Add `get_stake_weight` methods by @basfroman in https://github.com/opentensor/bittensor/pull/2985 +* Handles both exceptions for Swap pallet fetching by @thewhaleking in https://github.com/opentensor/bittensor/pull/2991 +* chore: fix typo by @socialsister in https://github.com/opentensor/bittensor/pull/2969 +* optimisations mostly related to liquidity_list by @thewhaleking in https://github.com/opentensor/bittensor/pull/2980 +* Transfers improvements by @thewhaleking in https://github.com/opentensor/bittensor/pull/2993 +* Remove ownership check in `transfer_stake_extrinsic` and `swap_stake_extrinsic` by @basfroman in https://github.com/opentensor/bittensor/pull/2996 +* Missed await by @thewhaleking in https://github.com/opentensor/bittensor/pull/3002 +* chore: fix typo by @lechpzn in https://github.com/opentensor/bittensor/pull/3001 +* Adds note for installing on macOS by @thewhaleking in https://github.com/opentensor/bittensor/pull/3004 +* Bump bittensor-wallet version by @thewhaleking in https://github.com/opentensor/bittensor/pull/3005 +* Format Error with string docs by @thewhaleking in https://github.com/opentensor/bittensor/pull/3006 +* `LoggingMachine` initialization updated to explicitly call both parent constructors by @basfroman in https://github.com/opentensor/bittensor/pull/3008 +* Fixed `moving_price` conversion from `I96F32` to float by @mcjkula in https://github.com/opentensor/bittensor/pull/3010 +* Add new CRv4 logic by @basfroman in https://github.com/opentensor/bittensor/pull/2999 +* UV Fix by @thewhaleking in https://github.com/opentensor/bittensor/pull/3011 + +## New Contributors +* @socialsister made their first contribution in https://github.com/opentensor/bittensor/pull/2969 +* @lechpzn made their first contribution in https://github.com/opentensor/bittensor/pull/3001 +* @mcjkula made their first contribution in https://github.com/opentensor/bittensor/pull/3010 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.8.3...v9.9.0 + ## 9.8.3 /2025-07-18 * improve make file by @basfroman in https://github.com/opentensor/bittensor/pull/2965 * Move all workflows from `app.circleci.com` to `GH actions` by @basfroman in https://github.com/opentensor/bittensor/pull/2970 diff --git a/README.md b/README.md index e401bea455..e74a7e1490 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,14 @@ python3 -m pip install --upgrade bittensor ## Install on macOS and Linux +### Note for macOS users +The macOS preinstalled CPython installation is compiled with LibreSSL instead of OpenSSL. There are a number +of issues with LibreSSL, and as such is not fully supported by the libraries used by bittensor. Thus we highly recommend, if +you are using a Mac, to first install Python from [Homebrew](https://brew.sh/). Additionally, the Rust FFI bindings +[if installing from precompiled wheels (default)] require the Homebrew-installed OpenSSL pacakge. If you choose to use +the preinstalled Python version from macOS, things may not work completely. + +### Installation You can install Bittensor SDK on your local machine in either of the following ways. **Make sure you verify your installation after you install**: - [Install using a Bash command](#install-using-a-bash-command). - [Install using `pip3 install`](#install-using-pip3-install) @@ -158,6 +166,13 @@ You can install using any of the below options: 1. Install `cubit` first. See the [Install](https://github.com/opentensor/cubit?tab=readme-ov-file#install) section. **Only Python 3.9 and 3.10 versions are supported**. 2. Then install SDK with `pip install bittensor`. + +### Troubleshooting +#### SSL: CERTIFICATE_VERIFY_FAILED + +If you are encountering a `[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate` +error, use the command `python -m bittensor certifi` which will update your local SSL certificates. + --- ## Install on Windows diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index bb46e4ddc9..98542c89a6 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -10,6 +10,7 @@ import scalecodec from async_substrate_interface import AsyncSubstrateInterface from async_substrate_interface.substrate_addons import RetryAsyncSubstrate +from async_substrate_interface.utils.storage import StorageKey from bittensor_drand import get_encrypted_commitment from bittensor_wallet.utils import SS58_FORMAT from numpy.typing import NDArray @@ -97,6 +98,7 @@ torch, u16_normalized_float, u64_normalized_float, + get_transfer_fn_params, ) from bittensor.core.extrinsics.asyncex.liquidity import ( add_liquidity_extrinsic, @@ -109,6 +111,7 @@ fixed_to_float, check_and_convert_to_balance, ) +from bittensor.utils import deprecated_message from bittensor.utils.btlogging import logging from bittensor.utils.liquidity import ( calculate_fees, @@ -767,7 +770,8 @@ async def state_call( block_hash = await self.determine_block_hash(block, block_hash, reuse_block) return await self.substrate.rpc_request( method="state_call", - params=[method, data, block_hash] if block_hash else [method, data], + params=[method, data], + block_hash=block_hash, reuse_block_hash=reuse_block, ) @@ -820,7 +824,7 @@ async def all_subnets( decoded = query.decode() - if not isinstance(subnet_prices, SubstrateRequestException): + if not isinstance(subnet_prices, (SubstrateRequestException, ValueError)): for sn in decoded: sn.update( {"price": subnet_prices.get(sn["netuid"], Balance.from_tao(0))} @@ -1154,7 +1158,7 @@ async def get_all_subnets_info( if not result: return [] - if not isinstance(prices, SubstrateRequestException): + if not isinstance(prices, (SubstrateRequestException, ValueError)): for subnet in result: subnet.update({"price": prices.get(subnet["netuid"], 0)}) else: @@ -1695,7 +1699,7 @@ async def get_current_weight_commit_info( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> list: + ) -> list[tuple[str, str, int]]: """ Retrieves CRV3 weight commit information for a specific subnet. @@ -1706,9 +1710,17 @@ async def get_current_weight_commit_info( reuse_block: Whether to reuse the last-used block hash. Returns: - list: A list of commit details, where each entry is a dictionary with keys 'who', 'serialized_commit', and - 'reveal_round', or an empty list if no data is found. + A list of commit details, where each item contains: + - ss58_address: The address of the committer. + - commit_message: The commit message. + - reveal_round: The round when the commitment was revealed. + + The list may be empty if there are no commits found. """ + deprecated_message( + message="The method `get_current_weight_commit_info` is deprecated and will be removed in version 10.0.0. " + "Use `get_current_weight_commit_info_v2` instead." + ) block_hash = await self.determine_block_hash(block, block_hash, reuse_block) result = await self.substrate.query_map( module="SubtensorModule", @@ -1721,6 +1733,43 @@ async def get_current_weight_commit_info( commits = result.records[0][1] if result.records else [] return [WeightCommitInfo.from_vec_u8(commit) for commit in commits] + async def get_current_weight_commit_info_v2( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> list[tuple[str, int, str, int]]: + """ + Retrieves CRV3 weight commit information for a specific subnet. + + Arguments: + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. Default is ``None``. + block_hash: The hash of the block to retrieve the subnet unique identifiers from. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + A list of commit details, where each item contains: + - ss58_address: The address of the committer. + - commit_block: The block number when the commitment was made. + - commit_message: The commit message. + - reveal_round: The round when the commitment was revealed. + + The list may be empty if there are no commits found. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + result = await self.substrate.query_map( + module="SubtensorModule", + storage_function="CRV3WeightCommitsV2", + params=[netuid], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + commits = result.records[0][1] if result.records else [] + return [WeightCommitInfo.from_vec_u8_v2(commit) for commit in commits] + async def get_delegate_by_hotkey( self, hotkey_ss58: str, @@ -2242,29 +2291,35 @@ async def get_liquidity_list( block=block, block_hash=block_hash, reuse_block=reuse_block ) - query = self.substrate.query + # Fetch global fees and current price + fee_global_tao_query_sk = await self.substrate.create_storage_key( + pallet="Swap", + storage_function="FeeGlobalTao", + params=[netuid], + block_hash=block_hash, + ) + fee_global_alpha_query_sk = await self.substrate.create_storage_key( + pallet="Swap", + storage_function="FeeGlobalAlpha", + params=[netuid], + block_hash=block_hash, + ) + sqrt_price_query_sk = await self.substrate.create_storage_key( + pallet="Swap", + storage_function="AlphaSqrtPrice", + params=[netuid], + block_hash=block_hash, + ) ( - fee_global_tao, - fee_global_alpha, - sqrt_price, + (fee_global_tao_query, fee_global_alpha_query, sqrt_price_query), positions_response, ) = await asyncio.gather( - query( - module="Swap", - storage_function="FeeGlobalTao", - params=[netuid], - block_hash=block_hash, - ), - query( - module="Swap", - storage_function="FeeGlobalAlpha", - params=[netuid], - block_hash=block_hash, - ), - query( - module="Swap", - storage_function="AlphaSqrtPrice", - params=[netuid], + self.substrate.query_multi( + [ + fee_global_tao_query_sk, + fee_global_alpha_query_sk, + sqrt_price_query_sk, + ], block_hash=block_hash, ), self.query_map( @@ -2275,36 +2330,46 @@ async def get_liquidity_list( ), ) # convert to floats - fee_global_tao = fixed_to_float(fee_global_tao) - fee_global_alpha = fixed_to_float(fee_global_alpha) - sqrt_price = fixed_to_float(sqrt_price) + fee_global_tao = fixed_to_float(fee_global_tao_query[1]) + fee_global_alpha = fixed_to_float(fee_global_alpha_query[1]) + sqrt_price = fixed_to_float(sqrt_price_query[1]) # Fetch global fees and current price current_tick = price_to_tick(sqrt_price**2) # Fetch positions - positions = [] + positions_values: list[tuple[dict, int, int]] = [] + positions_storage_keys: list[StorageKey] = [] async for _, p in positions_response: position = p.value tick_low_idx = position.get("tick_low")[0] tick_high_idx = position.get("tick_high")[0] - - tick_low, tick_high = await asyncio.gather( - query( - module="Swap", - storage_function="Ticks", - params=[netuid, tick_low_idx], - block_hash=block_hash, - ), - query( - module="Swap", - storage_function="Ticks", - params=[netuid, tick_high_idx], - block_hash=block_hash, - ), + positions_values.append((position, tick_low_idx, tick_high_idx)) + tick_low_sk = await self.substrate.create_storage_key( + pallet="Swap", + storage_function="Ticks", + params=[netuid, tick_low_idx], + block_hash=block_hash, + ) + tick_high_sk = await self.substrate.create_storage_key( + pallet="Swap", + storage_function="Ticks", + params=[netuid, tick_high_idx], + block_hash=block_hash, ) + positions_storage_keys.extend([tick_low_sk, tick_high_sk]) + # query all our ticks at once + ticks_query = await self.substrate.query_multi( + positions_storage_keys, block_hash=block_hash + ) + # iterator with just the values + ticks = iter([x[1] for x in ticks_query]) + positions = [] + for position, tick_low_idx, tick_high_idx in positions_values: + tick_low = next(ticks) + tick_high = next(ticks) # Calculate fees above/below range for both tokens tao_below = get_fees( current_tick=current_tick, @@ -2878,6 +2943,7 @@ async def get_stake_operations_fee( or reuse_block. reuse_block: Whether to reuse for this query the last-used block. Do not specify if also specifying block or block_hash. + Returns: The calculated stake fee as a Balance object. """ @@ -2892,6 +2958,38 @@ async def get_stake_operations_fee( ) return amount * (result.value / U16_MAX) + async def get_stake_weight( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> list[float]: + """ + Retrieves the stake weight for all hotkeys in a given subnet. + + Arguments: + netuid: Netuid of subnet. + block: Block number at which to perform the calculation. + block_hash: The hash of the blockchain block number for the query. Do not specify if also specifying block + or reuse_block. + reuse_block: Whether to reuse for this query the last-used block. Do not specify if also specifying block + or block_hash. + + Returns: + A list of stake weights for all hotkeys in the specified subnet. + """ + block_hash = await self.determine_block_hash( + block=block, block_hash=block_hash, reuse_block=reuse_block + ) + result = await self.substrate.query( + module="SubtensorModule", + storage_function="StakeWeight", + params=[netuid], + block_hash=block_hash, + ) + return [u16_normalized_float(w) for w in result] + async def get_subnet_burn_cost( self, block: Optional[int] = None, @@ -3037,7 +3135,7 @@ async def get_total_subnets( return getattr(result, "value", None) async def get_transfer_fee( - self, wallet: "Wallet", dest: str, value: Balance + self, wallet: "Wallet", dest: str, value: Balance, keep_alive: bool = True ) -> Balance: """ Calculates the transaction fee for transferring tokens from a wallet to a specified destination address. This @@ -3049,6 +3147,8 @@ async def get_transfer_fee( dest: The ``SS58`` address of the destination account. value: The amount of tokens to be transferred, specified as a Balance object, or in Tao (float) or Rao (int) units. + keep_alive: Whether the transfer fee should be calculated based on keeping the wallet alive (existential + deposit) or not. Returns: bittensor.utils.balance.Balance: The estimated transaction fee for the transfer, represented as a Balance @@ -3058,12 +3158,15 @@ async def get_transfer_fee( wallet has sufficient funds to cover both the transfer amount and the associated costs. This function provides a crucial tool for managing financial operations within the Bittensor network. """ - value = check_and_convert_to_balance(value) + if value is not None: + value = check_and_convert_to_balance(value) + call_params: dict[str, Union[int, str, bool]] + call_function, call_params = get_transfer_fn_params(value, dest, keep_alive) call = await self.substrate.compose_call( call_module="Balances", - call_function="transfer_keep_alive", - call_params={"dest": dest, "value": value.rao}, + call_function=call_function, + call_params=call_params, ) try: @@ -3779,7 +3882,7 @@ async def subnet( ) if isinstance(decoded := query.decode(), dict): - if isinstance(price, SubstrateRequestException): + if isinstance(price, (SubstrateRequestException, ValueError)): price = None return DynamicInfo.from_dict({**decoded, "price": price}) return None @@ -5358,7 +5461,7 @@ async def transfer( self, wallet: "Wallet", dest: str, - amount: Balance, + amount: Optional[Balance], transfer_all: bool = False, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, @@ -5371,7 +5474,7 @@ async def transfer( Arguments: wallet: Source wallet for the transfer. dest: Destination address for the transfer. - amount: Number of tokens to transfer. + amount: Number of tokens to transfer. `None` is transferring all. transfer_all: Flag to transfer all tokens. Default is `False`. wait_for_inclusion: Waits for the transaction to be included in a block. Defaults to `True`. wait_for_finalization: Waits for the transaction to be finalized on the blockchain. Defaults to `False`. @@ -5382,7 +5485,8 @@ async def transfer( Returns: `True` if the transferring was successful, otherwise `False`. """ - amount = check_and_convert_to_balance(amount) + if amount is not None: + amount = check_and_convert_to_balance(amount) return await transfer_extrinsic( subtensor=self, wallet=wallet, diff --git a/bittensor/core/chain_data/dynamic_info.py b/bittensor/core/chain_data/dynamic_info.py index 504185832f..416dfd29c4 100644 --- a/bittensor/core/chain_data/dynamic_info.py +++ b/bittensor/core/chain_data/dynamic_info.py @@ -126,7 +126,7 @@ def _from_dict(cls, decoded: dict) -> "DynamicInfo": network_registered_at=int(decoded["network_registered_at"]), subnet_identity=subnet_identity, subnet_volume=subnet_volume, - moving_price=fixed_to_float(decoded["moving_price"]), + moving_price=fixed_to_float(decoded["moving_price"], 32), ) def tao_to_alpha(self, tao: Union[Balance, float, int]) -> Balance: diff --git a/bittensor/core/chain_data/weight_commit_info.py b/bittensor/core/chain_data/weight_commit_info.py index db253f291c..b8555eed68 100644 --- a/bittensor/core/chain_data/weight_commit_info.py +++ b/bittensor/core/chain_data/weight_commit_info.py @@ -1,5 +1,6 @@ from dataclasses import dataclass from bittensor.core.chain_data.utils import decode_account_id +from typing import Optional @dataclass @@ -8,12 +9,14 @@ class WeightCommitInfo: Data class representing weight commit information. Attributes: - ss58 (str): The SS58 address of the committer - commit_hex (str): The serialized weight commit data as hex string - reveal_round (int): The round number for reveal + ss58: The SS58 address of the committer + commit_block: The block number of the commitment. + commit_hex: The serialized weight commit data as hex string + reveal_round: The round number for reveal """ ss58: str + commit_block: Optional[int] commit_hex: str reveal_round: int @@ -27,11 +30,37 @@ def from_vec_u8(cls, data: tuple) -> tuple[str, str, int]: Returns: WeightCommitInfo: A new instance with the decoded data + + Note: + This method is used when querying a block or block hash where storage functions `CRV3WeightCommitsV2` does + not exist in Subtensor module. """ - account_id, commit_data, round_number = data + account_id, commit_block, commit_data, round_number = data account_id_ = account_id[0] if isinstance(account_id, tuple) else account_id commit_data = commit_data[0] if isinstance(commit_data, tuple) else commit_data commit_hex = "0x" + "".join(format(x, "02x") for x in commit_data) return decode_account_id(account_id_), commit_hex, round_number + + @classmethod + def from_vec_u8_v2(cls, data: tuple) -> tuple[str, int, str, int]: + """ + Creates a WeightCommitInfo instance + + Args: + data (tuple): Tuple containing ((AccountId,), (commit_block, ) (commit_data,), round_number) + + Returns: + WeightCommitInfo: A new instance with the decoded data + """ + account_id, commit_block, commit_data, round_number = data + + account_id_ = account_id[0] if isinstance(account_id, tuple) else account_id + commit_block = ( + commit_block[0] if isinstance(commit_block, tuple) else commit_block + ) + commit_data = commit_data[0] if isinstance(commit_data, tuple) else commit_data + commit_hex = "0x" + "".join(format(x, "02x") for x in commit_data) + + return decode_account_id(account_id_), commit_block, commit_hex, round_number diff --git a/bittensor/core/extrinsics/asyncex/commit_reveal.py b/bittensor/core/extrinsics/asyncex/commit_reveal.py index 9c65ae8ea1..652ceafa50 100644 --- a/bittensor/core/extrinsics/asyncex/commit_reveal.py +++ b/bittensor/core/extrinsics/asyncex/commit_reveal.py @@ -16,12 +16,14 @@ from bittensor.utils.registration import torch +# TODO: Merge this logic with `commit_reveal_extrinsic` in SDKv10 bc this is not CRv3 anymore. async def _do_commit_reveal_v3( subtensor: "AsyncSubtensor", wallet: "Wallet", netuid: int, commit: bytes, reveal_round: int, + commit_reveal_version: int = 4, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, period: Optional[int] = None, @@ -35,6 +37,7 @@ async def _do_commit_reveal_v3( netuid: int The network unique identifier. commit: bytes The commit data in bytes format. reveal_round: int The round number for the reveal phase. + commit_reveal_version: The version of the chain commit-reveal protocol to use. Default is ``4``. wait_for_inclusion: bool, optional Flag indicating whether to wait for the extrinsic to be included in a block. wait_for_finalization: bool, optional Flag indicating whether to wait for the extrinsic to be finalized. period (Optional[int]): The number of blocks during which the transaction will remain valid after it's submitted. If @@ -52,11 +55,12 @@ async def _do_commit_reveal_v3( call = await subtensor.substrate.compose_call( call_module="SubtensorModule", - call_function="commit_crv3_weights", + call_function="commit_timelocked_weights", call_params={ "netuid": netuid, "commit": commit, "reveal_round": reveal_round, + "commit_reveal_version": commit_reveal_version, }, ) return await subtensor.sign_and_send_extrinsic( @@ -69,6 +73,7 @@ async def _do_commit_reveal_v3( ) +# TODO: rename this extrinsic to `commit_reveal_extrinsic` in SDK.v10 async def commit_reveal_v3_extrinsic( subtensor: "AsyncSubtensor", wallet: "Wallet", @@ -122,6 +127,7 @@ async def commit_reveal_v3_extrinsic( netuid=netuid, subnet_reveal_period_epochs=subnet_reveal_period_epochs, block_time=block_time, + hotkey=wallet.hotkey.public_key, ) success, message = await _do_commit_reveal_v3( @@ -135,7 +141,7 @@ async def commit_reveal_v3_extrinsic( period=period, ) - if success is not True: + if not success: logging.error(message) return False, message diff --git a/bittensor/core/extrinsics/asyncex/move_stake.py b/bittensor/core/extrinsics/asyncex/move_stake.py index 8cfd5a2604..8ed6a29141 100644 --- a/bittensor/core/extrinsics/asyncex/move_stake.py +++ b/bittensor/core/extrinsics/asyncex/move_stake.py @@ -71,14 +71,6 @@ async def transfer_stake_extrinsic( """ amount.set_unit(netuid=origin_netuid) - # Verify ownership - hotkey_owner = await subtensor.get_hotkey_owner(hotkey_ss58) - if hotkey_owner != wallet.coldkeypub.ss58_address: - logging.error( - f":cross_mark: [red]Failed[/red]: Hotkey: {hotkey_ss58} does not belong to the origin coldkey owner: " - f"{wallet.coldkeypub.ss58_address}" - ) - return False # Check sufficient stake stake_in_origin, stake_in_destination = await _get_stake_in_origin_and_dest( @@ -194,14 +186,6 @@ async def swap_stake_extrinsic( bool: True if the swap was successful, False otherwise. """ amount.set_unit(netuid=origin_netuid) - # Verify ownership - hotkey_owner = await subtensor.get_hotkey_owner(hotkey_ss58) - if hotkey_owner != wallet.coldkeypub.ss58_address: - logging.error( - f":cross_mark: [red]Failed[/red]: Hotkey: {hotkey_ss58} does not belong to the origin coldkey owner: " - f"{wallet.coldkeypub.ss58_address}" - ) - return False # Check sufficient stake stake_in_origin, stake_in_destination = await _get_stake_in_origin_and_dest( diff --git a/bittensor/core/extrinsics/asyncex/transfer.py b/bittensor/core/extrinsics/asyncex/transfer.py index e98e234307..6b0332cd73 100644 --- a/bittensor/core/extrinsics/asyncex/transfer.py +++ b/bittensor/core/extrinsics/asyncex/transfer.py @@ -6,6 +6,7 @@ get_explorer_url_for_network, is_valid_bittensor_address_or_public_key, unlock_key, + get_transfer_fn_params, ) from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging @@ -19,10 +20,11 @@ async def _do_transfer( subtensor: "AsyncSubtensor", wallet: "Wallet", destination: str, - amount: "Balance", + amount: Optional[Balance], wait_for_inclusion: bool = True, wait_for_finalization: bool = False, period: Optional[int] = None, + keep_alive: bool = True, ) -> tuple[bool, str, str]: """ Makes transfer from wallet to destination public key address. @@ -39,14 +41,17 @@ async def _do_transfer( period (Optional[int]): 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. + keep_alive (bool): If `True`, will keep the existential deposit in the account. Returns: success, block hash, formatted error message """ + call_function, call_params = get_transfer_fn_params(amount, destination, keep_alive) + call = await subtensor.substrate.compose_call( call_module="Balances", - call_function="transfer_keep_alive", - call_params={"dest": destination, "value": amount.rao}, + call_function=call_function, + call_params=call_params, ) success, message = await subtensor.sign_and_send_extrinsic( @@ -73,7 +78,7 @@ async def transfer_extrinsic( subtensor: "AsyncSubtensor", wallet: "Wallet", dest: str, - amount: "Balance", + amount: Optional[Balance], transfer_all: bool = False, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, @@ -86,7 +91,8 @@ async def transfer_extrinsic( subtensor (bittensor.core.async_subtensor.AsyncSubtensor): initialized AsyncSubtensor object used for transfer wallet (bittensor_wallet.Wallet): Bittensor wallet object to make transfer from. dest (str): Destination public key address (ss58_address or ed25519) of recipient. - amount (bittensor.utils.balance.Balance): Amount to stake as Bittensor balance. + amount (Optional[bittensor.utils.balance.Balance]): Amount to stake as Bittensor balance. `None` if + transferring all. transfer_all (bool): Whether to transfer all funds from this wallet to the destination address. wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning `True`, or returns `False` if the extrinsic fails to enter the block within the timeout. @@ -102,6 +108,11 @@ async def transfer_extrinsic( finalization / inclusion, the response is `True`, regardless of its inclusion. """ destination = dest + + if amount is None and not transfer_all: + logging.error("If not transferring all, `amount` must be specified.") + return False + # Validate destination address. if not is_valid_bittensor_address_or_public_key(destination): logging.error( @@ -128,7 +139,7 @@ async def transfer_extrinsic( ) fee = await subtensor.get_transfer_fee( - wallet=wallet, dest=destination, value=amount + wallet=wallet, dest=destination, value=amount, keep_alive=keep_alive ) if not keep_alive: @@ -137,12 +148,10 @@ async def transfer_extrinsic( # Check if we have enough balance. if transfer_all is True: - amount = account_balance - fee - existential_deposit - if amount < Balance(0): + if (account_balance - fee) < existential_deposit: logging.error("Not enough balance to transfer") return False - - if account_balance < (amount + fee + existential_deposit): + elif account_balance < (amount + fee + existential_deposit): logging.error(":cross_mark: [red]Not enough balance[/red]") logging.error(f"\t\tBalance:\t[blue]{account_balance}[/blue]") logging.error(f"\t\tAmount:\t[blue]{amount}[/blue]") @@ -155,6 +164,7 @@ async def transfer_extrinsic( wallet=wallet, destination=destination, amount=amount, + keep_alive=keep_alive, wait_for_finalization=wait_for_finalization, wait_for_inclusion=wait_for_inclusion, period=period, diff --git a/bittensor/core/extrinsics/commit_reveal.py b/bittensor/core/extrinsics/commit_reveal.py index c531986254..7709619d7e 100644 --- a/bittensor/core/extrinsics/commit_reveal.py +++ b/bittensor/core/extrinsics/commit_reveal.py @@ -16,19 +16,20 @@ from bittensor.utils.registration import torch +# TODO: Merge this logic with `commit_reveal_extrinsic` in SDKv10 bc this is not CRv3 anymore. def _do_commit_reveal_v3( subtensor: "Subtensor", wallet: "Wallet", netuid: int, commit: bytes, reveal_round: int, + commit_reveal_version: int = 4, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, period: Optional[int] = None, ) -> tuple[bool, str]: """ - Executes commit-reveal phase 3 for a given netuid and commit, and optionally waits for extrinsic inclusion or - finalization. + Executes commit-reveal extrinsic for a given netuid, commit, reveal_round, and commit_reveal_version. Arguments: subtensor: An instance of the Subtensor class. @@ -36,6 +37,7 @@ def _do_commit_reveal_v3( netuid: int The network unique identifier. commit: bytes The commit data in bytes format. reveal_round: int The round number for the reveal phase. + commit_reveal_version: The version of the chain commit-reveal protocol to use. Default is ``4``. wait_for_inclusion: bool, optional Flag indicating whether to wait for the extrinsic to be included in a block. wait_for_finalization: bool, optional Flag indicating whether to wait for the extrinsic to be finalized. period (Optional[int]): The number of blocks during which the transaction will remain valid after it's submitted. If @@ -53,11 +55,12 @@ def _do_commit_reveal_v3( call = subtensor.substrate.compose_call( call_module="SubtensorModule", - call_function="commit_crv3_weights", + call_function="commit_timelocked_weights", call_params={ "netuid": netuid, "commit": commit, "reveal_round": reveal_round, + "commit_reveal_version": commit_reveal_version, }, ) return subtensor.sign_and_send_extrinsic( @@ -70,6 +73,7 @@ def _do_commit_reveal_v3( ) +# TODO: rename this extrinsic to `commit_reveal_extrinsic` in SDK.v10 def commit_reveal_v3_extrinsic( subtensor: "Subtensor", wallet: "Wallet", @@ -123,6 +127,7 @@ def commit_reveal_v3_extrinsic( netuid=netuid, subnet_reveal_period_epochs=subnet_reveal_period_epochs, block_time=block_time, + hotkey=wallet.hotkey.public_key, ) success, message = _do_commit_reveal_v3( @@ -136,7 +141,7 @@ def commit_reveal_v3_extrinsic( period=period, ) - if success is not True: + if not success: logging.error(message) return False, message diff --git a/bittensor/core/extrinsics/move_stake.py b/bittensor/core/extrinsics/move_stake.py index 3e7e49eefd..d3874f1e68 100644 --- a/bittensor/core/extrinsics/move_stake.py +++ b/bittensor/core/extrinsics/move_stake.py @@ -68,14 +68,6 @@ def transfer_stake_extrinsic( """ amount.set_unit(netuid=origin_netuid) - # Verify ownership - hotkey_owner = subtensor.get_hotkey_owner(hotkey_ss58) - if hotkey_owner != wallet.coldkeypub.ss58_address: - logging.error( - f":cross_mark: [red]Failed[/red]: Hotkey: {hotkey_ss58} does not belong to the origin coldkey owner: " - f"{wallet.coldkeypub.ss58_address}" - ) - return False # Check sufficient stake stake_in_origin, stake_in_destination = _get_stake_in_origin_and_dest( @@ -192,14 +184,6 @@ def swap_stake_extrinsic( """ amount.set_unit(netuid=origin_netuid) - # Verify ownership - hotkey_owner = subtensor.get_hotkey_owner(hotkey_ss58) - if hotkey_owner != wallet.coldkeypub.ss58_address: - logging.error( - f":cross_mark: [red]Failed[/red]: Hotkey: {hotkey_ss58} does not belong to the origin coldkey owner: " - f"{wallet.coldkeypub.ss58_address}" - ) - return False # Check sufficient stake stake_in_origin, stake_in_destination = _get_stake_in_origin_and_dest( diff --git a/bittensor/core/extrinsics/transfer.py b/bittensor/core/extrinsics/transfer.py index 370efaed36..9faecdcea4 100644 --- a/bittensor/core/extrinsics/transfer.py +++ b/bittensor/core/extrinsics/transfer.py @@ -5,6 +5,7 @@ is_valid_bittensor_address_or_public_key, unlock_key, get_explorer_url_for_network, + get_transfer_fn_params, ) from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging @@ -18,10 +19,11 @@ def _do_transfer( subtensor: "Subtensor", wallet: "Wallet", destination: str, - amount: Balance, + amount: Optional[Balance], wait_for_inclusion: bool = True, wait_for_finalization: bool = False, period: Optional[int] = None, + keep_alive: bool = True, ) -> tuple[bool, str, str]: """ Makes transfer from wallet to destination public key address. @@ -30,7 +32,7 @@ def _do_transfer( subtensor (bittensor.core.subtensor.Subtensor): the Subtensor object used for transfer wallet (bittensor_wallet.Wallet): Bittensor wallet object to make transfer from. destination (str): Destination public key address (ss58_address or ed25519) of recipient. - amount (bittensor.utils.balance.Balance): Amount to stake as Bittensor balance. + amount (bittensor.utils.balance.Balance): Amount to stake as Bittensor balance. `None` if transferring all. wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning `True`, or returns `False` if the extrinsic fails to enter the block within the timeout. wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning @@ -38,14 +40,17 @@ def _do_transfer( period (Optional[int]): 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. + keep_alive (bool): If `True`, will keep the existential deposit in the account. Returns: success, block hash, formatted error message """ + call_function, call_params = get_transfer_fn_params(amount, destination, keep_alive) + call = subtensor.substrate.compose_call( call_module="Balances", - call_function="transfer_keep_alive", - call_params={"dest": destination, "value": amount.rao}, + call_function=call_function, + call_params=call_params, ) success, message = subtensor.sign_and_send_extrinsic( @@ -72,7 +77,7 @@ def transfer_extrinsic( subtensor: "Subtensor", wallet: "Wallet", dest: str, - amount: Balance, + amount: Optional[Balance], transfer_all: bool = False, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, @@ -85,7 +90,7 @@ def transfer_extrinsic( subtensor (bittensor.core.subtensor.Subtensor): the Subtensor object used for transfer wallet (bittensor_wallet.Wallet): Bittensor wallet object to make transfer from. dest (str): Destination public key address (ss58_address or ed25519) of recipient. - amount (bittensor.utils.balance.Balance): Amount to stake as Bittensor balance. + amount (bittensor.utils.balance.Balance): Amount to stake as Bittensor balance. `None` if transferring all. transfer_all (bool): Whether to transfer all funds from this wallet to the destination address. wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning `True`, or returns `False` if the extrinsic fails to enter the block within the timeout. @@ -100,6 +105,10 @@ def transfer_extrinsic( success (bool): Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for finalization / inclusion, the response is `True`, regardless of its inclusion. """ + if amount is None and not transfer_all: + logging.error("If not transferring all, `amount` must be specified.") + return False + # Validate destination address. if not is_valid_bittensor_address_or_public_key(dest): logging.error( @@ -127,16 +136,16 @@ def transfer_extrinsic( else: existential_deposit = subtensor.get_existential_deposit(block=block) - fee = subtensor.get_transfer_fee(wallet=wallet, dest=dest, value=amount) + fee = subtensor.get_transfer_fee( + wallet=wallet, dest=dest, value=amount, keep_alive=keep_alive + ) # Check if we have enough balance. if transfer_all is True: - amount = account_balance - fee - existential_deposit - if amount < Balance(0): + if (account_balance - fee) < existential_deposit: logging.error("Not enough balance to transfer") return False - - if account_balance < (amount + fee + existential_deposit): + elif account_balance < (amount + fee + existential_deposit): logging.error(":cross_mark: [red]Not enough balance[/red]") logging.error(f"\t\tBalance:\t[blue]{account_balance}[/blue]") logging.error(f"\t\tAmount:\t[blue]{amount}[/blue]") @@ -149,6 +158,7 @@ def transfer_extrinsic( wallet=wallet, destination=dest, amount=amount, + keep_alive=keep_alive, wait_for_finalization=wait_for_finalization, wait_for_inclusion=wait_for_inclusion, period=period, diff --git a/bittensor/core/extrinsics/unstaking.py b/bittensor/core/extrinsics/unstaking.py index fe70c69558..beff7f1993 100644 --- a/bittensor/core/extrinsics/unstaking.py +++ b/bittensor/core/extrinsics/unstaking.py @@ -75,7 +75,7 @@ def unstake_extrinsic( block=block, ) - # Covert to bittensor.Balance + # Convert to bittensor.Balance if amount is None: # Unstake it all. logging.warning( diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index b4b35d334a..d1b95521d8 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -9,6 +9,7 @@ from async_substrate_interface.substrate_addons import RetrySyncSubstrate from async_substrate_interface.sync_substrate import SubstrateInterface from async_substrate_interface.types import ScaleObj +from async_substrate_interface.utils.storage import StorageKey from bittensor_drand import get_encrypted_commitment from numpy.typing import NDArray @@ -105,6 +106,8 @@ torch, u16_normalized_float, u64_normalized_float, + deprecated_message, + get_transfer_fn_params, ) from bittensor.utils.balance import ( Balance, @@ -428,8 +431,7 @@ def state_call( """ block_hash = self.determine_block_hash(block) return self.substrate.rpc_request( - method="state_call", - params=[method, data, block_hash] if block_hash else [method, data], + method="state_call", params=[method, data], block_hash=block_hash ) # Common subtensor calls =========================================================================================== @@ -462,7 +464,7 @@ def all_subnets(self, block: Optional[int] = None) -> Optional[list["DynamicInfo sn.update( {"price": subnet_prices.get(sn["netuid"], Balance.from_tao(0))} ) - except SubstrateRequestException as e: + except (SubstrateRequestException, ValueError) as e: logging.warning(f"Unable to fetch subnet prices for block {block}: {e}") return DynamicInfo.list_from_dicts(decoded) @@ -655,7 +657,7 @@ def get_all_subnets_info(self, block: Optional[int] = None) -> list["SubnetInfo" for subnet in result: subnet.update({"price": subnets_prices.get(subnet["netuid"], 0)}) - except SubstrateRequestException as e: + except (SubstrateRequestException, ValueError) as e: logging.warning(f"Unable to fetch subnet prices for block {block}: {e}") return SubnetInfo.list_from_dicts(result) @@ -1080,7 +1082,7 @@ def get_all_revealed_commitments( def get_current_weight_commit_info( self, netuid: int, block: Optional[int] = None - ) -> list: + ) -> list[tuple[str, str, int]]: """ Retrieves CRV3 weight commit information for a specific subnet. @@ -1089,9 +1091,18 @@ def get_current_weight_commit_info( block (Optional[int]): The blockchain block number for the query. Default is ``None``. Returns: - list: A list of commit details, where each entry is a dictionary with keys 'who', 'serialized_commit', and - 'reveal_round', or an empty list if no data is found. + A list of commit details, where each item contains: + - ss58_address: The address of the committer. + - commit_message: The commit message. + - reveal_round: The round when the commitment was revealed. + + The list may be empty if there are no commits found. + """ + deprecated_message( + message="The method `get_current_weight_commit_info` is deprecated and will be removed in version 10.0.0. " + "Use `get_current_weight_commit_info_v2` instead." + ) result = self.substrate.query_map( module="SubtensorModule", storage_function="CRV3WeightCommits", @@ -1102,6 +1113,35 @@ def get_current_weight_commit_info( commits = result.records[0][1] if result.records else [] return [WeightCommitInfo.from_vec_u8(commit) for commit in commits] + def get_current_weight_commit_info_v2( + self, netuid: int, block: Optional[int] = None + ) -> list[tuple[str, int, str, int]]: + """ + Retrieves CRV3 weight commit information for a specific subnet. + + Arguments: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. Default is ``None``. + + Returns: + A list of commit details, where each item contains: + - ss58_address: The address of the committer. + - commit_block: The block number when the commitment was made. + - commit_message: The commit message. + - reveal_round: The round when the commitment was revealed. + + The list may be empty if there are no commits found. + """ + result = self.substrate.query_map( + module="SubtensorModule", + storage_function="CRV3WeightCommitsV2", + params=[netuid], + block_hash=self.determine_block_hash(block), + ) + + commits = result.records[0][1] if result.records else [] + return [WeightCommitInfo.from_vec_u8_v2(commit) for commit in commits] + def get_delegate_by_hotkey( self, hotkey_ss58: str, block: Optional[int] = None ) -> Optional["DelegateInfo"]: @@ -1492,31 +1532,41 @@ def get_liquidity_list( logging.debug(f"Subnet {netuid} is not active.") return None - query = self.substrate.query block_hash = self.determine_block_hash(block) # Fetch global fees and current price - fee_global_tao_query = query( - module="Swap", + fee_global_tao_query_sk = self.substrate.create_storage_key( + pallet="Swap", storage_function="FeeGlobalTao", params=[netuid], block_hash=block_hash, ) - fee_global_alpha_query = query( - module="Swap", + fee_global_alpha_query_sk = self.substrate.create_storage_key( + pallet="Swap", storage_function="FeeGlobalAlpha", params=[netuid], block_hash=block_hash, ) - sqrt_price_query = query( - module="Swap", + sqrt_price_query_sk = self.substrate.create_storage_key( + pallet="Swap", storage_function="AlphaSqrtPrice", params=[netuid], block_hash=block_hash, ) - fee_global_tao = fixed_to_float(fee_global_tao_query) - fee_global_alpha = fixed_to_float(fee_global_alpha_query) - sqrt_price = fixed_to_float(sqrt_price_query) + fee_global_tao_query, fee_global_alpha_query, sqrt_price_query = ( + self.substrate.query_multi( + [ + fee_global_tao_query_sk, + fee_global_alpha_query_sk, + sqrt_price_query_sk, + ], + block_hash=block_hash, + ) + ) + + fee_global_tao = fixed_to_float(fee_global_tao_query[1]) + fee_global_alpha = fixed_to_float(fee_global_alpha_query[1]) + sqrt_price = fixed_to_float(sqrt_price_query[1]) current_tick = price_to_tick(sqrt_price**2) # Fetch positions @@ -1526,26 +1576,38 @@ def get_liquidity_list( block=block, params=[netuid, wallet.coldkeypub.ss58_address], ) - - positions = [] + positions_values: list[tuple[dict, int, int]] = [] + positions_storage_keys: list[StorageKey] = [] for _, p in positions_response: position = p.value tick_low_idx = position["tick_low"][0] tick_high_idx = position["tick_high"][0] - tick_low = query( - module="Swap", + tick_low_sk = self.substrate.create_storage_key( + pallet="Swap", storage_function="Ticks", params=[netuid, tick_low_idx], block_hash=block_hash, ) - tick_high = query( - module="Swap", + tick_high_sk = self.substrate.create_storage_key( + pallet="Swap", storage_function="Ticks", params=[netuid, tick_high_idx], block_hash=block_hash, ) + positions_values.append((position, tick_low_idx, tick_high_idx)) + positions_storage_keys.extend([tick_low_sk, tick_high_sk]) + # query all our ticks at once + ticks_query = self.substrate.query_multi( + positions_storage_keys, block_hash=block_hash + ) + # iterator with just the values + ticks = iter([x[1] for x in ticks_query]) + positions = [] + for position, tick_low_idx, tick_high_idx in positions_values: + tick_low = next(ticks) + tick_high = next(ticks) # Calculate fees above/below range for both tokens tao_below = get_fees( @@ -2050,6 +2112,26 @@ def get_stake_operations_fee( ) return amount * (result.value / U16_MAX) + def get_stake_weight(self, netuid: int, block: Optional[int] = None) -> list[float]: + """ + Retrieves the stake weight for all hotkeys in a given subnet. + + Arguments: + netuid: Netuid of subnet. + block: Block number at which to perform the calculation. + + Returns: + A list of stake weights for all hotkeys in the specified subnet. + """ + block_hash = self.determine_block_hash(block=block) + result = self.substrate.query( + module="SubtensorModule", + storage_function="StakeWeight", + params=[netuid], + block_hash=block_hash, + ) + return [u16_normalized_float(w) for w in result] + def get_subnet_burn_cost(self, block: Optional[int] = None) -> Optional[Balance]: """ Retrieves the burn cost for registering a new subnet within the Bittensor network. This cost represents the @@ -2162,7 +2244,13 @@ def get_total_subnets(self, block: Optional[int] = None) -> Optional[int]: ) return getattr(result, "value", None) - def get_transfer_fee(self, wallet: "Wallet", dest: str, value: Balance) -> Balance: + def get_transfer_fee( + self, + wallet: "Wallet", + dest: str, + value: Optional[Balance], + keep_alive: bool = True, + ) -> Balance: """ Calculates the transaction fee for transferring tokens from a wallet to a specified destination address. This function simulates the transfer to estimate the associated cost, taking into account the current network @@ -2173,6 +2261,8 @@ def get_transfer_fee(self, wallet: "Wallet", dest: str, value: Balance) -> Balan dest (str): The ``SS58`` address of the destination account. value (Union[bittensor.utils.balance.Balance, float, int]): The amount of tokens to be transferred, specified as a Balance object, or in Tao (float) or Rao (int) units. + keep_alive: Whether the transfer fee should be calculated based on keeping the wallet alive (existential + deposit) or not. Returns: bittensor.utils.balance.Balance: The estimated transaction fee for the transfer, represented as a Balance @@ -2182,11 +2272,15 @@ def get_transfer_fee(self, wallet: "Wallet", dest: str, value: Balance) -> Balan has sufficient funds to cover both the transfer amount and the associated costs. This function provides a crucial tool for managing financial operations within the Bittensor network. """ - value = check_and_convert_to_balance(value) + if value is not None: + value = check_and_convert_to_balance(value) + call_params: dict[str, Union[int, str, bool]] + call_function, call_params = get_transfer_fn_params(value, dest, keep_alive) + call = self.substrate.compose_call( call_module="Balances", - call_function="transfer_keep_alive", - call_params={"dest": dest, "value": value.rao}, + call_function=call_function, + call_params=call_params, ) try: @@ -2713,7 +2807,7 @@ def subnet(self, netuid: int, block: Optional[int] = None) -> Optional[DynamicIn if isinstance(decoded := query.decode(), dict): try: price = self.get_subnet_price(netuid=netuid, block=block) - except SubstrateRequestException: + except (SubstrateRequestException, ValueError): price = None return DynamicInfo.from_dict({**decoded, "price": price}) return None @@ -4185,7 +4279,7 @@ def transfer( self, wallet: "Wallet", dest: str, - amount: Balance, + amount: Optional[Balance], wait_for_inclusion: bool = True, wait_for_finalization: bool = False, transfer_all: bool = False, @@ -4211,7 +4305,8 @@ def transfer( Returns: `True` if the transferring was successful, otherwise `False`. """ - amount = check_and_convert_to_balance(amount) + if amount is not None: + amount = check_and_convert_to_balance(amount) return transfer_extrinsic( subtensor=self, wallet=wallet, diff --git a/bittensor/core/subtensor_api/commitments.py b/bittensor/core/subtensor_api/commitments.py index 6bdbb7f9f3..8a9c49bd30 100644 --- a/bittensor/core/subtensor_api/commitments.py +++ b/bittensor/core/subtensor_api/commitments.py @@ -12,6 +12,9 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.get_all_revealed_commitments = subtensor.get_all_revealed_commitments self.get_commitment = subtensor.get_commitment self.get_current_weight_commit_info = subtensor.get_current_weight_commit_info + self.get_current_weight_commit_info_v2 = ( + subtensor.get_current_weight_commit_info_v2 + ) self.get_last_commitment_bonds_reset_block = ( subtensor.get_last_commitment_bonds_reset_block ) diff --git a/bittensor/core/subtensor_api/staking.py b/bittensor/core/subtensor_api/staking.py index a99e201610..979d8a2632 100644 --- a/bittensor/core/subtensor_api/staking.py +++ b/bittensor/core/subtensor_api/staking.py @@ -20,6 +20,7 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.get_stake_info_for_coldkey = subtensor.get_stake_info_for_coldkey self.get_stake_movement_fee = subtensor.get_stake_movement_fee self.get_stake_operations_fee = subtensor.get_stake_operations_fee + self.get_stake_weight = subtensor.get_stake_weight self.get_unstake_fee = subtensor.get_unstake_fee self.unstake = subtensor.unstake self.unstake_all = subtensor.unstake_all diff --git a/bittensor/core/subtensor_api/subnets.py b/bittensor/core/subtensor_api/subnets.py index ac63a7ac28..cfdee525b8 100644 --- a/bittensor/core/subtensor_api/subnets.py +++ b/bittensor/core/subtensor_api/subnets.py @@ -12,6 +12,7 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.blocks_since_last_step = subtensor.blocks_since_last_step self.blocks_since_last_update = subtensor.blocks_since_last_update self.bonds = subtensor.bonds + self.commit_reveal_enabled = subtensor.commit_reveal_enabled self.difficulty = subtensor.difficulty self.get_all_subnets_info = subtensor.get_all_subnets_info self.get_parents = subtensor.get_parents diff --git a/bittensor/core/subtensor_api/utils.py b/bittensor/core/subtensor_api/utils.py index 6aad488979..adef4f31b1 100644 --- a/bittensor/core/subtensor_api/utils.py +++ b/bittensor/core/subtensor_api/utils.py @@ -48,6 +48,9 @@ def add_legacy_methods(subtensor: "SubtensorApi"): subtensor.get_current_weight_commit_info = ( subtensor._subtensor.get_current_weight_commit_info ) + subtensor.get_current_weight_commit_info_v2 = ( + subtensor._subtensor.get_current_weight_commit_info_v2 + ) subtensor.get_delegate_by_hotkey = subtensor._subtensor.get_delegate_by_hotkey subtensor.get_delegate_identities = subtensor._subtensor.get_delegate_identities subtensor.get_delegate_take = subtensor._subtensor.get_delegate_take @@ -87,6 +90,7 @@ def add_legacy_methods(subtensor: "SubtensorApi"): ) subtensor.get_stake_movement_fee = subtensor._subtensor.get_stake_movement_fee subtensor.get_stake_operations_fee = subtensor._subtensor.get_stake_operations_fee + subtensor.get_stake_weight = subtensor._subtensor.get_stake_weight subtensor.get_subnet_burn_cost = subtensor._subtensor.get_subnet_burn_cost subtensor.get_subnet_hyperparameters = ( subtensor._subtensor.get_subnet_hyperparameters diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index 14fb383847..57d528f76f 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -1,6 +1,7 @@ import ast import decimal import hashlib +import warnings from collections import namedtuple from typing import Any, Literal, Union, Optional, TYPE_CHECKING from urllib.parse import urlparse @@ -21,6 +22,7 @@ if TYPE_CHECKING: from bittensor_wallet import Wallet + from bittensor.utils.balance import Balance BT_DOCS_LINK = "https://docs.bittensor.com" @@ -253,7 +255,9 @@ def format_error_message(error_message: Union[dict, Exception]) -> str: err_type = error_message.get("type", err_type) err_name = error_message.get("name", err_name) err_docs = error_message.get("docs", [err_description]) - err_description = " ".join(err_docs) + err_description = ( + err_docs if isinstance(err_docs, str) else " ".join(err_docs) + ) err_description += ( f" | Please consult {BT_DOCS_LINK}/errors/subtensor#{err_name.lower()}" ) @@ -438,3 +442,40 @@ def determine_chain_endpoint_and_network( return result return "unknown", network + + +def deprecated_message(message: str) -> None: + """Shows a deprecation warning message with the given message.""" + warnings.simplefilter("default", DeprecationWarning) + warnings.warn(message=message, category=DeprecationWarning, stacklevel=2) + + +def get_transfer_fn_params( + amount: Optional["Balance"], destination: str, keep_alive: bool +) -> tuple[str, dict[str, Union[str, int, bool]]]: + """ + Helper function to get the transfer call function and call params, depending on the value and keep_alive flag + provided + + Args: + amount: the amount of Tao to transfer. `None` if transferring all. + destination: the destination SS58 of the transfer + keep_alive: whether to enforce a retention of the existential deposit in the account after transfer. + + Returns: + tuple[call function, call params] + """ + call_params = {"dest": destination} + if amount is None: + call_function = "transfer_all" + if keep_alive: + call_params["keep_alive"] = True + else: + call_params["keep_alive"] = False + else: + call_params["value"] = amount.rao + if keep_alive: + call_function = "transfer_keep_alive" + else: + call_function = "transfer_allow_death" + return call_function, call_params diff --git a/bittensor/utils/balance.py b/bittensor/utils/balance.py index b65fdaec82..99c1a85831 100644 --- a/bittensor/utils/balance.py +++ b/bittensor/utils/balance.py @@ -4,6 +4,7 @@ from scalecodec import ScaleType from bittensor.core import settings +from bittensor.utils import deprecated_message def _check_currencies(self, other): @@ -349,8 +350,7 @@ def fixed_to_float( ) -> float: # By default, this is a U64F64 # which is 64 bits of integer and 64 bits of fractional - - data: int = fixed["bits"] + data: int = fb.value if isinstance((fb := fixed["bits"]), ScaleType) else fb # Logical and to get the fractional part; remaining is the integer part fractional_part = data & (2**frac_bits - 1) @@ -830,12 +830,9 @@ def check_and_convert_to_balance( This is used to support backwards compatibility while also providing a deprecation notice. """ if isinstance(amount, (float, int)): - warnings.simplefilter("default", DeprecationWarning) - warnings.warn( + deprecated_message( "Detected a non-balance amount. Converting to Balance from Tao for backwards compatibility." - "Please update your code to use tao(amount) or Balance.from_tao(amount) for the main release 9.0.0.", - category=DeprecationWarning, - stacklevel=2, + "Please update your code to use tao(amount) or Balance.from_tao(amount) for the main release 10.0.0." ) amount = tao(amount) return amount diff --git a/bittensor/utils/btlogging/loggingmachine.py b/bittensor/utils/btlogging/loggingmachine.py index a8803e70fd..32ea7315e0 100644 --- a/bittensor/utils/btlogging/loggingmachine.py +++ b/bittensor/utils/btlogging/loggingmachine.py @@ -137,7 +137,8 @@ class LoggingMachine(StateMachine, Logger): def __init__(self, config: "Config", name: str = BITTENSOR_LOGGER_NAME): # basics - super(LoggingMachine, self).__init__() + StateMachine.__init__(self) + stdlogging.Logger.__init__(self, name) self._queue = mp.Queue(-1) self._primary_loggers = {name} self._config = self._extract_logging_config(config) diff --git a/contrib/RELEASE_GUIDELINES.md b/contrib/RELEASE_GUIDELINES.md index 74658624d8..f40003bd68 100644 --- a/contrib/RELEASE_GUIDELINES.md +++ b/contrib/RELEASE_GUIDELINES.md @@ -44,7 +44,7 @@ Since you need to use a secret when releasing bittensor (github personal access So you can have: ``` -GITHUB_ACCESS_TOKEN=$(pass github/your_personal_token_with_permisions) +GITHUB_ACCESS_TOKEN=$(pass github/your_personal_token_with_permissions) ``` or diff --git a/pyproject.toml b/pyproject.toml index de109b014b..71f245a935 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bittensor" -version = "9.8.3" +version = "9.9.0" description = "Bittensor" readme = "README.md" authors = [ @@ -34,9 +34,9 @@ dependencies = [ "pydantic>=2.3, <3", "scalecodec==1.2.11", "uvicorn", - "bittensor-drand>=0.5.0", - "bittensor-wallet>=3.1.0", - "async-substrate-interface>=1.3.1" + "bittensor-drand>=1.0.0,<2.0.0", + "bittensor-wallet>=4.0.0,<5.0", + "async-substrate-interface>=1.4.2" ] [project.optional-dependencies] diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index c79446be26..c9ed635971 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -18,7 +18,10 @@ setup_wallet, ) -LOCALNET_IMAGE_NAME = os.getenv("LOCALNET_IMAGE_NAME") or "ghcr.io/opentensor/subtensor-localnet:devnet-ready" +LOCALNET_IMAGE_NAME = ( + os.getenv("LOCALNET_IMAGE_NAME") + or "ghcr.io/opentensor/subtensor-localnet:devnet-ready" +) CONTAINER_NAME_PREFIX = "test_local_chain_" diff --git a/tests/e2e_tests/test_commit_reveal_v3.py b/tests/e2e_tests/test_commit_reveal_v3.py index 719dbe5827..b729d0a874 100644 --- a/tests/e2e_tests/test_commit_reveal_v3.py +++ b/tests/e2e_tests/test_commit_reveal_v3.py @@ -15,7 +15,7 @@ # @pytest.mark.parametrize("local_chain", [True], indirect=True) @pytest.mark.asyncio -async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_wallet): +async def test_commit_and_reveal_weights_cr4(local_chain, subtensor, alice_wallet): """ Tests the commit/reveal weights mechanism (CR3) @@ -30,56 +30,64 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle Raises: AssertionError: If any of the checks or verifications fail """ + logging.console.info("Testing `test_commit_and_reveal_weights_cr4`") + BLOCK_TIME = ( 0.25 if subtensor.is_fast_blocks() else 12.0 ) # 12 for non-fast-block, 0.25 for fast block - netuid = subtensor.get_total_subnets() # 2 - logging.console.info("Testing test_commit_and_reveal_weights") + logging.console.info(f"Using block time: {BLOCK_TIME}") + + netuid = subtensor.get_total_subnets() # 2 # Register root as Alice assert subtensor.register_subnet(alice_wallet), "Unable to register the subnet" # Verify subnet 2 created successfully - assert subtensor.subnet_exists(netuid), ( - f"Subnet {netuid} wasn't created successfully" - ) + assert subtensor.subnet_exists(netuid), f"SN #{netuid} wasn't created successfully" - logging.console.success(f"Subnet {netuid} is registered") + logging.console.success(f"SN #{netuid} is registered.") # Enable commit_reveal on the subnet assert sudo_set_hyperparameter_bool( - local_chain, - alice_wallet, - "sudo_set_commit_reveal_weights_enabled", - True, - netuid, - ), "Unable to enable commit reveal on the subnet" + substrate=local_chain, + wallet=alice_wallet, + call_function="sudo_set_commit_reveal_weights_enabled", + value=True, + netuid=netuid, + ), f"Unable to enable commit reveal on the SN #{netuid}" # Verify commit_reveal was enabled - assert subtensor.commit_reveal_enabled(netuid), "Failed to enable commit/reveal" - logging.console.info("Commit reveal enabled") + assert subtensor.subnets.commit_reveal_enabled(netuid), ( + "Failed to enable commit/reveal" + ) + logging.console.success("Commit reveal enabled") + + cr_version = subtensor.substrate.query( + module="SubtensorModule", storage_function="CommitRevealWeightsVersion" + ) + assert cr_version == 4, f"Commit reveal version is not 3, got {cr_version}" # Change the weights rate limit on the subnet status, error = sudo_set_admin_utils( - local_chain, - alice_wallet, + substrate=local_chain, + wallet=alice_wallet, call_function="sudo_set_weights_set_rate_limit", call_params={"netuid": netuid, "weights_set_rate_limit": "0"}, ) - assert error is None assert status is True + assert error is None # Verify weights rate limit was changed assert ( subtensor.get_subnet_hyperparameters(netuid=netuid).weights_rate_limit == 0 ), "Failed to set weights_rate_limit" assert subtensor.weights_rate_limit(netuid=netuid) == 0 - logging.console.info("sudo_set_weights_set_rate_limit executed: set to 0") + logging.console.success("sudo_set_weights_set_rate_limit executed: set to 0") # Change the tempo of the subnet - tempo_set = 50 if subtensor.is_fast_blocks() else 10 + tempo_set = 100 if subtensor.is_fast_blocks() else 10 assert ( sudo_set_admin_utils( local_chain, @@ -89,9 +97,10 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle )[0] is True ) + tempo = subtensor.get_subnet_hyperparameters(netuid=netuid).tempo - assert tempo_set == tempo - logging.console.info(f"sudo_set_tempo executed: set to {tempo_set}") + assert tempo_set == tempo, "SN tempos has not been changed." + logging.console.success(f"SN #{netuid} tempo set to {tempo_set}") # Commit-reveal values - setting weights to self uids = np.array([0], dtype=np.int64) @@ -107,11 +116,8 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle f"Checking if window is too low with Current block: {current_block}, next tempo: {upcoming_tempo}" ) - # Wait for 2 tempos to pass as CR3 only reveals weights after 2 tempos + 1 - subtensor.wait_for_block(tempo_set * 2 + 1) - # Lower than this might mean weights will get revealed before we can check them - if upcoming_tempo - current_block < 3: + if upcoming_tempo - current_block < 6: await wait_interval( tempo, subtensor, @@ -119,6 +125,7 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle reporting_interval=1, ) current_block = subtensor.get_current_block() + expected_commit_block = current_block + 1 latest_drand_round = subtensor.last_drand_round() upcoming_tempo = next_tempo(current_block, tempo) logging.console.info( @@ -127,8 +134,8 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle # Commit weights success, message = subtensor.set_weights( - alice_wallet, - netuid, + wallet=alice_wallet, + netuid=netuid, uids=weight_uids, weights=weight_vals, wait_for_inclusion=True, @@ -138,46 +145,35 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle ) # Assert committing was a success - assert success is True + assert success is True, message assert bool(re.match(r"reveal_round:\d+", message)) # Parse expected reveal_round expected_reveal_round = int(message.split(":")[1]) - logging.console.info( + logging.console.success( f"Successfully set weights: uids {weight_uids}, weights {weight_vals}, reveal_round: {expected_reveal_round}" ) - current_block = subtensor.get_current_block() - latest_drand_round = subtensor.last_drand_round() - upcoming_tempo = next_tempo(current_block, tempo) - logging.console.info( - f"After setting weights: Current block: {current_block}, next tempo: {upcoming_tempo}, drand: {latest_drand_round}" - ) - - # Ensure the expected drand round is well in the future - assert expected_reveal_round >= latest_drand_round + 1, ( - "Revealed drand pulse is older than the drand pulse right after setting weights" - ) - # Fetch current commits pending on the chain - commits_on_chain = subtensor.get_current_weight_commit_info(netuid=netuid) - address, commit, reveal_round = commits_on_chain[0] + commits_on_chain = subtensor.get_current_weight_commit_info_v2(netuid=netuid) + address, commit_block, commit, reveal_round = commits_on_chain[0] # Assert correct values are committed on the chain assert expected_reveal_round == reveal_round assert address == alice_wallet.hotkey.ss58_address + assert commit_block == expected_commit_block + 1 # Ensure no weights are available as of now assert subtensor.weights(netuid=netuid) == [] + logging.console.success("No weights are available before next epoch.") - # Wait for the next tempo so weights can be revealed - await wait_interval( - subtensor.get_subnet_hyperparameters(netuid=netuid).tempo, - subtensor, - netuid=netuid, - reporting_interval=1, - sleep=BLOCK_TIME, + expected_reveal_block = ( + subtensor.subnets.get_next_epoch_start_block(netuid) + 5 + ) # 5 is safety drand offset + logging.console.info( + f"Waiting for the next epoch to ensure weights are revealed: block {expected_reveal_block}" ) + subtensor.wait_for_block(expected_reveal_block) # Fetch the latest drand pulse latest_drand_round = subtensor.last_drand_round() @@ -185,25 +181,24 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle f"Latest drand round after waiting for tempo: {latest_drand_round}" ) - # wait until last_drand_round is the same or greeter than expected_reveal_round with sleep 3 second (as Drand round period) - while expected_reveal_round >= subtensor.last_drand_round(): - time.sleep(3) - # Fetch weights on the chain as they should be revealed now - revealed_weights_ = subtensor.weights(netuid=netuid) + subnet_weights = subtensor.weights(netuid=netuid) - print("revealed weights", revealed_weights_) - revealed_weights = revealed_weights_[0][1] + revealed_weights = subnet_weights[0][1] # Assert correct weights were revealed assert weight_uids[0] == revealed_weights[0][0] assert weight_vals[0] == revealed_weights[0][1] + logging.console.success( + f"Successfully revealed weights: uids {weight_uids}, weights {weight_vals}" + ) + # Now that the commit has been revealed, there shouldn't be any pending commits - assert subtensor.get_current_weight_commit_info(netuid=netuid) == [] + assert subtensor.commitments.get_current_weight_commit_info_v2(netuid=netuid) == [] # Ensure the drand_round is always in the positive w.r.t expected when revealed - assert latest_drand_round - expected_reveal_round >= 0, ( + assert latest_drand_round - expected_reveal_round >= -3, ( f"latest_drand_round ({latest_drand_round}) is less than expected_reveal_round ({expected_reveal_round})" ) - logging.console.info("✅ Passed commit_reveal v3") + logging.console.success("✅ Passed `test_commit_and_reveal_weights_cr4`") diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py index fd9ad09d42..780aa272c8 100644 --- a/tests/e2e_tests/test_liquidity.py +++ b/tests/e2e_tests/test_liquidity.py @@ -73,7 +73,7 @@ async def test_liquidity(local_chain, subtensor, alice_wallet, bob_wallet): wallet=alice_wallet, hotkey_ss58=alice_wallet.hotkey.ss58_address, netuid=alice_subnet_netuid, - amount=Balance.from_tao(1000), + amount=Balance.from_tao(1), wait_for_inclusion=True, wait_for_finalization=True, ), "❌ Cannot cannot add stake to Alice from Alice." diff --git a/tests/e2e_tests/test_transfer.py b/tests/e2e_tests/test_transfer.py index 7a0728de72..0663b540b2 100644 --- a/tests/e2e_tests/test_transfer.py +++ b/tests/e2e_tests/test_transfer.py @@ -1,10 +1,18 @@ +import typing + +from bittensor_wallet import Wallet +import pytest + from bittensor.utils.balance import Balance from bittensor import logging +if typing.TYPE_CHECKING: + from bittensor.core.subtensor_api import SubtensorApi + logging.set_trace() -def test_transfer(subtensor, alice_wallet): +def test_transfer(subtensor: "SubtensorApi", alice_wallet): """ Test the transfer mechanism on the chain @@ -47,3 +55,90 @@ def test_transfer(subtensor, alice_wallet): ) print("✅ Passed test_transfer") + + +def test_transfer_all(subtensor: "Subtensor", alice_wallet): + # create two dummy accounts we can drain + dummy_account_1 = Wallet(path="/tmp/bittensor-dummy-account-1") + dummy_account_2 = Wallet(path="/tmp/bittensor-dummy-account-2") + dummy_account_1.create_new_coldkey(use_password=False, overwrite=True) + dummy_account_2.create_new_coldkey(use_password=False, overwrite=True) + + # fund the first dummy account + assert subtensor.transfer( + alice_wallet, + dest=dummy_account_1.coldkeypub.ss58_address, + amount=Balance.from_tao(2.0), + wait_for_finalization=True, + wait_for_inclusion=True, + ) + # Account details before transfer + existential_deposit = subtensor.get_existential_deposit() + assert subtensor.transfer( + wallet=dummy_account_1, + dest=dummy_account_2.coldkeypub.ss58_address, + amount=None, + transfer_all=True, + wait_for_finalization=True, + wait_for_inclusion=True, + keep_alive=True, + ) + balance_after = subtensor.get_balance(dummy_account_1.coldkeypub.ss58_address) + assert balance_after == existential_deposit + assert subtensor.transfer( + wallet=dummy_account_2, + dest=alice_wallet.coldkeypub.ss58_address, + amount=None, + transfer_all=True, + wait_for_inclusion=True, + wait_for_finalization=True, + keep_alive=False, + ) + balance_after = subtensor.get_balance(dummy_account_2.coldkeypub.ss58_address) + assert balance_after == Balance(0) + + +@pytest.mark.asyncio +async def test_async_transfer(async_subtensor: "SubtensorApi", alice_wallet): + # create two dummy accounts we can drain + dummy_account_1 = Wallet(path="/tmp/bittensor-dummy-account-3") + dummy_account_2 = Wallet(path="/tmp/bittensor-dummy-account-4") + dummy_account_1.create_new_coldkey(use_password=False, overwrite=True) + dummy_account_2.create_new_coldkey(use_password=False, overwrite=True) + + # fund the first dummy account + assert await async_subtensor.transfer( + alice_wallet, + dest=dummy_account_1.coldkeypub.ss58_address, + amount=Balance.from_tao(2.0), + wait_for_finalization=True, + wait_for_inclusion=True, + ) + # Account details before transfer + existential_deposit = await async_subtensor.get_existential_deposit() + assert await async_subtensor.transfer( + wallet=dummy_account_1, + dest=dummy_account_2.coldkeypub.ss58_address, + amount=None, + transfer_all=True, + wait_for_finalization=True, + wait_for_inclusion=True, + keep_alive=True, + ) + balance_after = await async_subtensor.get_balance( + dummy_account_1.coldkeypub.ss58_address + ) + assert balance_after == existential_deposit + assert await async_subtensor.transfer( + wallet=dummy_account_2, + dest=alice_wallet.coldkeypub.ss58_address, + amount=None, + transfer_all=True, + wait_for_inclusion=True, + wait_for_finalization=True, + keep_alive=False, + ) + balance_after = await async_subtensor.get_balance( + dummy_account_2.coldkeypub.ss58_address + ) + assert balance_after == Balance(0) diff --git a/tests/e2e_tests/utils/e2e_test_utils.py b/tests/e2e_tests/utils/e2e_test_utils.py index 8e5254535f..d41c80982d 100644 --- a/tests/e2e_tests/utils/e2e_test_utils.py +++ b/tests/e2e_tests/utils/e2e_test_utils.py @@ -274,13 +274,14 @@ async def async_wait_to_start_call( if await subtensor.is_fast_blocks() is False: in_blocks = 5 + current_block = await subtensor.block + bittensor.logging.console.info( f"Waiting for [blue]{in_blocks}[/blue] blocks before [red]start call[/red]. " - f"Current block: [blue]{subtensor.block}[/blue]." + f"Current block: [blue]{current_block}[/blue]." ) # make sure we passed start_call limit - current_block = await subtensor.block await subtensor.wait_for_block(current_block + in_blocks + 1) status, message = await subtensor.start_call( wallet=subnet_owner_wallet, diff --git a/tests/unit_tests/extrinsics/asyncex/test_commit_reveal.py b/tests/unit_tests/extrinsics/asyncex/test_commit_reveal.py index e105d07e71..137a5d8d41 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_commit_reveal.py +++ b/tests/unit_tests/extrinsics/asyncex/test_commit_reveal.py @@ -73,11 +73,12 @@ async def test_do_commit_reveal_v3_success(mocker, subtensor, fake_wallet): # Asserts mocked_compose_call.assert_awaited_once_with( call_module="SubtensorModule", - call_function="commit_crv3_weights", + call_function="commit_timelocked_weights", call_params={ "netuid": fake_netuid, "commit": fake_commit, "reveal_round": fake_reveal_round, + "commit_reveal_version": 4, }, ) mocked_create_signed_extrinsic.assert_awaited_once_with( @@ -132,11 +133,12 @@ async def test_do_commit_reveal_v3_failure_due_to_error(mocker, subtensor, fake_ # Asserts mocked_compose_call.assert_awaited_once_with( call_module="SubtensorModule", - call_function="commit_crv3_weights", + call_function="commit_timelocked_weights", call_params={ "netuid": fake_netuid, "commit": fake_commit, "reveal_round": fake_reveal_round, + "commit_reveal_version": 4, }, ) mocked_create_signed_extrinsic.assert_awaited_once_with( @@ -220,6 +222,7 @@ async def test_commit_reveal_v3_extrinsic_success_with_torch( netuid=fake_netuid, current_block=mock_block.return_value["header"]["number"], block_time=12.0, + hotkey=fake_wallet.hotkey.public_key, ) mock_do_commit_reveal_v3.assert_awaited_once_with( subtensor=subtensor, diff --git a/tests/unit_tests/extrinsics/asyncex/test_registration.py b/tests/unit_tests/extrinsics/asyncex/test_registration.py index d10c032cd7..4cecae6616 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_registration.py +++ b/tests/unit_tests/extrinsics/asyncex/test_registration.py @@ -1,6 +1,5 @@ import pytest -from bittensor.core import async_subtensor from bittensor.core.extrinsics.asyncex import registration as async_registration diff --git a/tests/unit_tests/extrinsics/asyncex/test_transfer.py b/tests/unit_tests/extrinsics/asyncex/test_transfer.py index 95c5249b62..299c6df446 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_transfer.py +++ b/tests/unit_tests/extrinsics/asyncex/test_transfer.py @@ -3,12 +3,32 @@ from bittensor.utils.balance import Balance +@pytest.mark.parametrize( + "amount,keep_alive,call_function,call_params", + [ + ( + Balance(1), + True, + "transfer_keep_alive", + {"dest": "SS58PUBLICKEY", "value": Balance(1).rao}, + ), + (None, True, "transfer_all", {"dest": "SS58PUBLICKEY", "keep_alive": True}), + (None, False, "transfer_all", {"dest": "SS58PUBLICKEY", "keep_alive": False}), + ( + Balance(1), + False, + "transfer_allow_death", + {"dest": "SS58PUBLICKEY", "value": Balance(1).rao}, + ), + ], +) @pytest.mark.asyncio -async def test_do_transfer_success(subtensor, fake_wallet, mocker): +async def test_do_transfer_success( + subtensor, fake_wallet, mocker, amount, keep_alive, call_function, call_params +): """Tests _do_transfer when the transfer is successful.""" # Preps - fake_destination = "destination_address" - fake_amount = mocker.Mock(autospec=Balance, rao=1000) + fake_destination = "SS58PUBLICKEY" fake_block_hash = "fake_block_hash" mocker.patch.object(subtensor.substrate, "compose_call") @@ -24,7 +44,8 @@ async def test_do_transfer_success(subtensor, fake_wallet, mocker): subtensor=subtensor, wallet=fake_wallet, destination=fake_destination, - amount=fake_amount, + amount=amount, + keep_alive=keep_alive, wait_for_inclusion=True, wait_for_finalization=True, ) @@ -32,8 +53,8 @@ async def test_do_transfer_success(subtensor, fake_wallet, mocker): # Asserts subtensor.substrate.compose_call.assert_awaited_once_with( call_module="Balances", - call_function="transfer_keep_alive", - call_params={"dest": fake_destination, "value": fake_amount.rao}, + call_function=call_function, + call_params=call_params, ) subtensor.sign_and_send_extrinsic.assert_awaited_once_with( diff --git a/tests/unit_tests/extrinsics/test__init__.py b/tests/unit_tests/extrinsics/test__init__.py index ba17c001be..e0273d9a83 100644 --- a/tests/unit_tests/extrinsics/test__init__.py +++ b/tests/unit_tests/extrinsics/test__init__.py @@ -102,3 +102,22 @@ def test_format_error_message_with_custom_error_message_without_index(): == f"Subtensor returned `SubstrateRequestException({fake_custom_error['message']})` error. This means: " f"`{fake_custom_error['data']}`." ) + + +def test_format_error_with_string_docs(): + fake_error_message = { + "type": "SomeType", + "name": "SomeErrorName", + "docs": "Some error description.", + } + + # Call + result = format_error_message(fake_error_message) + + # Assertions + + assert ( + result == "Subtensor returned `SomeErrorName(SomeType)` error. " + "This means: `Some error description." + f" | Please consult {BT_DOCS_LINK}/errors/subtensor#someerrorname`." + ) diff --git a/tests/unit_tests/extrinsics/test_commit_reveal.py b/tests/unit_tests/extrinsics/test_commit_reveal.py index 49cc92e11f..42ae3e14d9 100644 --- a/tests/unit_tests/extrinsics/test_commit_reveal.py +++ b/tests/unit_tests/extrinsics/test_commit_reveal.py @@ -70,11 +70,12 @@ def test_do_commit_reveal_v3_success(mocker, subtensor, fake_wallet): # Asserts mocked_compose_call.assert_called_once_with( call_module="SubtensorModule", - call_function="commit_crv3_weights", + call_function="commit_timelocked_weights", call_params={ "netuid": fake_netuid, "commit": fake_commit, "reveal_round": fake_reveal_round, + "commit_reveal_version": 4, }, ) mocked_sign_and_send_extrinsic.assert_called_once_with( @@ -123,11 +124,12 @@ def test_do_commit_reveal_v3_failure_due_to_error(mocker, subtensor, fake_wallet # Asserts mocked_compose_call.assert_called_once_with( call_module="SubtensorModule", - call_function="commit_crv3_weights", + call_function="commit_timelocked_weights", call_params={ "netuid": fake_netuid, "commit": fake_commit, "reveal_round": fake_reveal_round, + "commit_reveal_version": 4, }, ) mocked_create_signed_extrinsic.assert_called_once_with( @@ -205,6 +207,7 @@ def test_commit_reveal_v3_extrinsic_success_with_torch( netuid=fake_netuid, current_block=mock_block.return_value, block_time=12.0, + hotkey=fake_wallet.hotkey.public_key, ) mock_do_commit_reveal_v3.assert_called_once_with( subtensor=subtensor, diff --git a/tests/unit_tests/extrinsics/test_set_weights.py b/tests/unit_tests/extrinsics/test_set_weights.py index 1046385291..6a47809b43 100644 --- a/tests/unit_tests/extrinsics/test_set_weights.py +++ b/tests/unit_tests/extrinsics/test_set_weights.py @@ -1,9 +1,8 @@ from unittest.mock import MagicMock, patch import pytest -import torch +# import torch -from bittensor.core import subtensor as subtensor_module from bittensor.core.extrinsics.set_weights import ( _do_set_weights, set_weights_extrinsic, diff --git a/tests/unit_tests/extrinsics/test_transfer.py b/tests/unit_tests/extrinsics/test_transfer.py index 081a56ffae..6aedaea601 100644 --- a/tests/unit_tests/extrinsics/test_transfer.py +++ b/tests/unit_tests/extrinsics/test_transfer.py @@ -1,12 +1,34 @@ from bittensor.core.extrinsics.transfer import _do_transfer from bittensor.utils.balance import Balance - -def test_do_transfer_is_success_true(subtensor, fake_wallet, mocker): +import pytest + + +@pytest.mark.parametrize( + "amount,keep_alive,call_function,call_params", + [ + ( + Balance(1), + True, + "transfer_keep_alive", + {"dest": "SS58PUBLICKEY", "value": Balance(1).rao}, + ), + (None, True, "transfer_all", {"dest": "SS58PUBLICKEY", "keep_alive": True}), + (None, False, "transfer_all", {"dest": "SS58PUBLICKEY", "keep_alive": False}), + ( + Balance(1), + False, + "transfer_allow_death", + {"dest": "SS58PUBLICKEY", "value": Balance(1).rao}, + ), + ], +) +def test_do_transfer_is_success_true( + subtensor, fake_wallet, mocker, amount, keep_alive, call_function, call_params +): """Successful do_transfer call.""" # Prep fake_dest = "SS58PUBLICKEY" - fake_transfer_balance = Balance(1) fake_wait_for_inclusion = True fake_wait_for_finalization = True @@ -18,16 +40,17 @@ def test_do_transfer_is_success_true(subtensor, fake_wallet, mocker): subtensor, fake_wallet, fake_dest, - fake_transfer_balance, + amount, fake_wait_for_inclusion, fake_wait_for_finalization, + keep_alive=keep_alive, ) # Asserts subtensor.substrate.compose_call.assert_called_once_with( call_module="Balances", - call_function="transfer_keep_alive", - call_params={"dest": fake_dest, "value": fake_transfer_balance.rao}, + call_function=call_function, + call_params=call_params, ) subtensor.sign_and_send_extrinsic.assert_called_once_with( call=subtensor.substrate.compose_call.return_value, @@ -45,6 +68,7 @@ def test_do_transfer_is_success_false(subtensor, fake_wallet, mocker): # Prep fake_dest = "SS58PUBLICKEY" fake_transfer_balance = Balance(1) + keep_alive = True fake_wait_for_inclusion = True fake_wait_for_finalization = True @@ -59,6 +83,7 @@ def test_do_transfer_is_success_false(subtensor, fake_wallet, mocker): fake_transfer_balance, fake_wait_for_inclusion, fake_wait_for_finalization, + keep_alive=keep_alive, ) # Asserts @@ -83,6 +108,7 @@ def test_do_transfer_no_waits(subtensor, fake_wallet, mocker): # Prep fake_dest = "SS58PUBLICKEY" fake_transfer_balance = Balance(1) + keep_alive = True fake_wait_for_inclusion = False fake_wait_for_finalization = False @@ -98,6 +124,7 @@ def test_do_transfer_no_waits(subtensor, fake_wallet, mocker): fake_transfer_balance, fake_wait_for_inclusion, fake_wait_for_finalization, + keep_alive=keep_alive, ) # Asserts diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 24dee30ef7..05dbc6f760 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -3570,52 +3570,75 @@ async def test_get_liquidity_list_happy_path(subtensor, fake_wallet, mocker): return_value=(Balance.from_tao(0.0), Balance.from_tao(0.0, netuid)), ) - mocked_substrate_query = mocker.AsyncMock( + mocked_substrate_query_multi = mocker.AsyncMock( side_effect=[ - # for gather - {"bits": 0}, - {"bits": 0}, - {"bits": 18446744073709551616}, - # for loop - { - "liquidity_net": 1000000000000, - "liquidity_gross": 1000000000000, - "fees_out_tao": {"bits": 0}, - "fees_out_alpha": {"bits": 0}, - }, - { - "liquidity_net": -1000000000000, - "liquidity_gross": 1000000000000, - "fees_out_tao": {"bits": 0}, - "fees_out_alpha": {"bits": 0}, - }, - { - "liquidity_net": 1000000000000, - "liquidity_gross": 1000000000000, - "fees_out_tao": {"bits": 0}, - "fees_out_alpha": {"bits": 0}, - }, - { - "liquidity_net": -1000000000000, - "liquidity_gross": 1000000000000, - "fees_out_tao": {"bits": 0}, - "fees_out_alpha": {"bits": 0}, - }, - { - "liquidity_net": 1000000000000, - "liquidity_gross": 1000000000000, - "fees_out_tao": {"bits": 0}, - "fees_out_alpha": {"bits": 0}, - }, - { - "liquidity_net": -1000000000000, - "liquidity_gross": 1000000000000, - "fees_out_tao": {"bits": 0}, - "fees_out_alpha": {"bits": 0}, - }, + [ + (None, {"bits": 0}), + (None, {"bits": 0}), + (None, {"bits": 18446744073709551616}), + ], + [ + ( + None, + { + "liquidity_net": 1000000000000, + "liquidity_gross": 1000000000000, + "fees_out_tao": {"bits": 0}, + "fees_out_alpha": {"bits": 0}, + }, + ), + ( + None, + { + "liquidity_net": -1000000000000, + "liquidity_gross": 1000000000000, + "fees_out_tao": {"bits": 0}, + "fees_out_alpha": {"bits": 0}, + }, + ), + ( + None, + { + "liquidity_net": 1000000000000, + "liquidity_gross": 1000000000000, + "fees_out_tao": {"bits": 0}, + "fees_out_alpha": {"bits": 0}, + }, + ), + ( + None, + { + "liquidity_net": -1000000000000, + "liquidity_gross": 1000000000000, + "fees_out_tao": {"bits": 0}, + "fees_out_alpha": {"bits": 0}, + }, + ), + ( + None, + { + "liquidity_net": 1000000000000, + "liquidity_gross": 1000000000000, + "fees_out_tao": {"bits": 0}, + "fees_out_alpha": {"bits": 0}, + }, + ), + ( + None, + { + "liquidity_net": -1000000000000, + "liquidity_gross": 1000000000000, + "fees_out_tao": {"bits": 0}, + "fees_out_alpha": {"bits": 0}, + }, + ), + ], ] ) - mocker.patch.object(subtensor.substrate, "query", mocked_substrate_query) + + mocker.patch.object( + subtensor.substrate, "query_multi", mocked_substrate_query_multi + ) fake_positions = [ [ @@ -4060,3 +4083,39 @@ async def test_get_stake_movement_fee(subtensor, mocker): amount=amount, netuid=netuid, block=None ) assert result == mocked_get_stake_operations_fee.return_value + + +@pytest.mark.asyncio +async def test_get_stake_weight(subtensor, mocker): + """Verify that `get_stake_weight` method calls proper methods and returns the correct value.""" + # Preps + netuid = mocker.Mock() + fake_weights = [0, 100, 15000] + expected_result = [0.0, 0.0015259021896696422, 0.22888532845044632] + + mock_determine_block_hash = mocker.patch.object( + subtensor, + "determine_block_hash", + ) + mocked_query = mocker.patch.object( + subtensor.substrate, + "query", + return_value=fake_weights, + ) + + # Call + result = await subtensor.get_stake_weight(netuid=netuid) + + # Asserts + mock_determine_block_hash.assert_awaited_once_with( + block=None, + block_hash=None, + reuse_block=False, + ) + mocked_query.assert_awaited_once_with( + module="SubtensorModule", + storage_function="StakeWeight", + params=[netuid], + block_hash=mock_determine_block_hash.return_value, + ) + assert result == expected_result diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 6ee02cea5a..aa5121408e 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -954,8 +954,7 @@ def test_state_call(subtensor, mocker): # Asserts subtensor.substrate.rpc_request.assert_called_once_with( - method="state_call", - params=[fake_method, fake_data], + method="state_call", params=[fake_method, fake_data], block_hash=None ) assert result == subtensor.substrate.rpc_request.return_value @@ -1497,7 +1496,7 @@ def test_do_serve_axon_is_success( ) assert result[0] is True - assert result[1] is "" + assert result[1] == "" def test_do_serve_axon_is_not_success(subtensor, fake_wallet, mocker, fake_call_params): @@ -3845,25 +3844,22 @@ def test_get_liquidity_list_subnet_is_not_active(subtensor, mocker): def test_get_liquidity_list_happy_path(subtensor, fake_wallet, mocker): """Tests `get_liquidity_list` returns the correct value.""" - # Preps netuid = 2 + # Mock network state mocker.patch.object(subtensor, "subnet_exists", return_value=True) mocker.patch.object(subtensor, "is_subnet_active", return_value=True) - mocker.patch.object(subtensor, "determine_block_hash") + mocker.patch.object(subtensor, "determine_block_hash", return_value="0x1234") - mocker.patch.object( - subtensor_module, "price_to_tick", return_value=Balance.from_tao(1.0, netuid) - ) + # Mock price and fee calculation + mocker.patch.object(subtensor_module, "price_to_tick", return_value=100) mocker.patch.object( subtensor_module, "calculate_fees", return_value=(Balance.from_tao(0.0), Balance.from_tao(0.0, netuid)), ) - mocked_substrate_query = mocker.MagicMock() - mocker.patch.object(subtensor.substrate, "query", mocked_substrate_query) - + # Fake positions to return from query_map fake_positions = [ [ (2,), @@ -3879,55 +3875,58 @@ def test_get_liquidity_list_happy_path(subtensor, fake_wallet, mocker): } ), ], - [ - (2,), - mocker.Mock( - value={ - "id": (2,), - "netuid": 2, - "tick_low": (216189,), - "tick_high": (198196,), - "liquidity": 2000000000000, - "fees_tao": {"bits": 0}, - "fees_alpha": {"bits": 0}, - } - ), - ], - [ - (2,), - mocker.Mock( - value={ - "id": (2,), - "netuid": 2, - "tick_low": (226189,), - "tick_high": (188196,), - "liquidity": 3000000000000, - "fees_tao": {"bits": 0}, - "fees_alpha": {"bits": 0}, - } - ), - ], ] mocked_query_map = mocker.MagicMock(return_value=fake_positions) mocker.patch.object(subtensor, "query_map", new=mocked_query_map) - # Call + # Mock storage key creation + mocker.patch.object( + subtensor.substrate, + "create_storage_key", + side_effect=lambda pallet, + storage_function, + params, + block_hash=None: f"{pallet}:{storage_function}:{params}", + ) + + # Mock query_multi for fee + sqrt_price + tick data + mock_query_multi = mocker.MagicMock( + side_effect=[ + [ + ("key1", {"bits": 0}), # fee_global_tao + ("key2", {"bits": 0}), # fee_global_alpha + ("key3", {"bits": 1072693248}), + ], + [ + ( + "tick_low", + {"fees_out_tao": {"bits": 0}, "fees_out_alpha": {"bits": 0}}, + ), + ( + "tick_high", + {"fees_out_tao": {"bits": 0}, "fees_out_alpha": {"bits": 0}}, + ), + ], + ] + ) + mocker.patch.object(subtensor.substrate, "query_multi", new=mock_query_multi) + # Call result = subtensor.get_liquidity_list(wallet=fake_wallet, netuid=netuid) # Asserts - subtensor.determine_block_hash.assert_called_once_with(None) + assert subtensor.determine_block_hash.call_count == 1 assert subtensor_module.price_to_tick.call_count == 1 assert subtensor_module.calculate_fees.call_count == len(fake_positions) - mocked_query_map.assert_called_once_with( module="Swap", name="Positions", block=None, params=[netuid, fake_wallet.coldkeypub.ss58_address], ) + assert mock_query_multi.call_count == 2 # one for fees, one for ticks assert len(result) == len(fake_positions) - assert all([isinstance(p, subtensor_module.LiquidityPosition) for p in result]) + assert all(isinstance(p, subtensor_module.LiquidityPosition) for p in result) def test_add_liquidity(subtensor, fake_wallet, mocker): @@ -4275,3 +4274,34 @@ def test_get_stake_movement_fee(subtensor, mocker): amount=amount, netuid=netuid, block=None ) assert result == mocked_get_stake_operations_fee.return_value + + +def test_get_stake_weight(subtensor, mocker): + """Verify that `get_stake_weight` method calls proper methods and returns the correct value.""" + # Preps + netuid = mocker.Mock() + fake_weights = [0, 100, 15000] + expected_result = [0.0, 0.0015259021896696422, 0.22888532845044632] + + mock_determine_block_hash = mocker.patch.object( + subtensor, + "determine_block_hash", + ) + mocked_query = mocker.patch.object( + subtensor.substrate, + "query", + return_value=fake_weights, + ) + + # Call + result = subtensor.get_stake_weight(netuid=netuid) + + # Asserts + mock_determine_block_hash.assert_called_once_with(block=None) + mocked_query.assert_called_once_with( + module="SubtensorModule", + storage_function="StakeWeight", + params=[netuid], + block_hash=mock_determine_block_hash.return_value, + ) + assert result == expected_result diff --git a/tests/unit_tests/test_subtensor_extended.py b/tests/unit_tests/test_subtensor_extended.py index 78851574cf..ec6015174b 100644 --- a/tests/unit_tests/test_subtensor_extended.py +++ b/tests/unit_tests/test_subtensor_extended.py @@ -424,13 +424,16 @@ def test_get_children_pending(mock_substrate, subtensor): ) -def test_get_current_weight_commit_info(mock_substrate, subtensor, fake_wallet, mocker): +def test_get_current_weight_commit_info_v2( + mock_substrate, subtensor, fake_wallet, mocker +): mock_substrate.query_map.return_value.records = [ ( mocker.ANY, [ ( bytearray(32), + 100, b"data", 123, ), @@ -438,13 +441,14 @@ def test_get_current_weight_commit_info(mock_substrate, subtensor, fake_wallet, ), ] - result = subtensor.get_current_weight_commit_info( + result = subtensor.get_current_weight_commit_info_v2( netuid=1, ) assert result == [ ( "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + 100, "0x64617461", 123, ), @@ -452,7 +456,7 @@ def test_get_current_weight_commit_info(mock_substrate, subtensor, fake_wallet, mock_substrate.query_map.assert_called_once_with( module="SubtensorModule", - storage_function="CRV3WeightCommits", + storage_function="CRV3WeightCommitsV2", params=[1], block_hash=None, ) @@ -1480,31 +1484,6 @@ def test_transfer_stake_error( ) -def test_transfer_stake_non_owner(mock_substrate, subtensor, fake_wallet, mocker): - mocker.patch.object( - subtensor, - "get_hotkey_owner", - autospec=True, - return_value="owner2_ss58", - ) - - success = subtensor.transfer_stake( - fake_wallet, - "dest", - "hotkey_ss58", - origin_netuid=1, - destination_netuid=1, - amount=Balance(1), - ) - - assert success is False - - subtensor.get_hotkey_owner.assert_called_once_with( - "hotkey_ss58", - ) - mock_substrate.submit_extrinsic.assert_not_called() - - def test_transfer_stake_insufficient_stake( mock_substrate, subtensor, fake_wallet, mocker ): diff --git a/tests/unit_tests/utils/test_weight_utils.py b/tests/unit_tests/utils/test_weight_utils.py index e49a814e00..c378173bfa 100644 --- a/tests/unit_tests/utils/test_weight_utils.py +++ b/tests/unit_tests/utils/test_weight_utils.py @@ -1,12 +1,10 @@ import logging import numpy as np -from hypothesis import settings import bittensor.utils.weight_utils as weight_utils import pytest from bittensor.utils import torch -from bittensor.core.settings import version_as_int def test_convert_weight_and_uids():