diff --git a/.github/workflows/docker_release.yml b/.github/workflows/docker_release.yml index dbb6c3bab8..9e8aabed5a 100644 --- a/.github/workflows/docker_release.yml +++ b/.github/workflows/docker_release.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install cosign uses: sigstore/cosign-installer@v3 @@ -48,4 +48,4 @@ jobs: DIGEST: ${{ steps.build.outputs.digest }} TAGS: ${{ steps.build.outputs.tags }} run: | - echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} \ No newline at end of file + echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} diff --git a/.github/workflows/e2e-subtensor-tests.yaml b/.github/workflows/e2e-subtensor-tests.yaml index 08e8c2606f..494da0b011 100644 --- a/.github/workflows/e2e-subtensor-tests.yaml +++ b/.github/workflows/e2e-subtensor-tests.yaml @@ -12,6 +12,9 @@ on: branches: [master, development, staging] types: [ opened, synchronize, reopened, ready_for_review ] + schedule: + - cron: '0 9 * * *' # Run every night at 2:00 PST + workflow_dispatch: inputs: verbose: @@ -40,7 +43,7 @@ jobs: run: | test_files=$(find tests/e2e_tests -name "test*.py" | jq -R -s -c 'split("\n") | map(select(. != ""))') # keep it here for future debug - # test_files=$(find tests/e2e_tests -type f -name "test*.py" | grep -E 'test_(incentive|commit_weights|set_weights)\.py$' | jq -R -s -c 'split("\n") | map(select(. != ""))') + # test_files=$(find tests/e2e_tests -type f -name "test*.py" | grep -E 'test_(hotkeys|staking)\.py$' | jq -R -s -c 'split("\n") | map(select(. != ""))') echo "test-files=$test_files" >> "$GITHUB_OUTPUT" shell: bash @@ -63,8 +66,8 @@ jobs: path: subtensor-localnet.tar # Job to run tests in parallel - run-e2e-test: - name: ${{ matrix.test-file }} / Python ${{ matrix.python-version }} + run-fast-blocks-e2e-test: + name: "FB: ${{ matrix.test-file }} / Python ${{ matrix.python-version }}" needs: - find-tests - pull-docker-image @@ -107,7 +110,80 @@ jobs: - name: Run tests with retry run: | set +e - for i in 1 2; do + for i in 1 2 3; do + echo "🔁 Attempt $i: Running tests" + uv run pytest ${{ matrix.test-file }} -s + status=$? + if [ $status -eq 0 ]; then + echo "✅ Tests passed on attempt $i" + break + else + echo "❌ Tests failed on attempt $i" + if [ $i -eq 3 ]; then + echo "Tests failed after 3 attempts" + exit 1 + fi + echo "Retrying..." + sleep 5 + fi + done + + + cron-run-non-fast-blocks-e2e-test: + if: github.event_name == 'schedule' + name: "NFB: ${{ matrix.test-file }} / Python ${{ matrix.python-version }}" + needs: + - find-tests + - pull-docker-image + runs-on: ubuntu-latest + timeout-minutes: 1440 + + strategy: + fail-fast: false # Allow other matrix jobs to run even if this job fails + max-parallel: 32 # Set the maximum number of parallel jobs (same as we have cores in ubuntu-latest runner) + matrix: + os: + - ubuntu-latest + test-file: ${{ fromJson(needs.find-tests.outputs.test-files) }} + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + + steps: + - name: Check if today is Saturday + run: | + day=$(date -u +%u) + echo "Today is weekday $day" + if [ "$day" -ne 6 ]; then + echo "â­ī¸ Skipping: not Saturday" + exit 78 + fi + - name: Check-out repository + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: install dependencies + run: uv sync --extra dev --dev + + - name: Download Cached Docker Image + uses: actions/download-artifact@v4 + with: + name: subtensor-localnet + + - name: Load Docker Image + run: docker load -i subtensor-localnet.tar + + - name: Run patched E2E tests + env: + FAST_BLOCKS: "0" + run: | + set +e + for i in 1 2 3; do echo "🔁 Attempt $i: Running tests" uv run pytest ${{ matrix.test-file }} -s status=$? @@ -116,8 +192,8 @@ jobs: break else echo "❌ Tests failed on attempt $i" - if [ $i -eq 2 ]; then - echo "Tests failed after 2 attempts" + if [ $i -eq 3 ]; then + echo "Tests failed after 3 attempts" exit 1 fi echo "Retrying..." diff --git a/.github/workflows/monitor_requirements_size_master.yml b/.github/workflows/monitor_requirements_size_master.yml new file mode 100644 index 0000000000..301767ce7e --- /dev/null +++ b/.github/workflows/monitor_requirements_size_master.yml @@ -0,0 +1,88 @@ +# This workflow measures the disk size of a virtual environment +# after installing the Bittensor SDK across multiple Python versions. +# It runs only when a new pull request targets the master branch, +# and posts a comment with the results. +name: Monitor SDK Requirements Size + +on: + pull_request: + types: [opened] + branches: [master] + +permissions: + pull-requests: write + contents: read + +jobs: + measure-venv: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + outputs: + py39: ${{ steps.set-output.outputs.py39 }} + py310: ${{ steps.set-output.outputs.py310 }} + py311: ${{ steps.set-output.outputs.py311 }} + py312: ${{ steps.set-output.outputs.py312 }} + py313: ${{ steps.set-output.outputs.py313 }} + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Create virtualenv and install + run: | + python -m venv venv + source venv/bin/activate + pip install --upgrade pip + pip install . + + - name: Measure venv size + id: set-output + run: | + SIZE=$(du -sm venv | cut -f1) + VERSION=${{ matrix.python-version }} + echo "Detected size: $SIZE MB for Python $VERSION" + case "$VERSION" in + 3.9) echo "py39=$SIZE" >> $GITHUB_OUTPUT ;; + 3.10) echo "py310=$SIZE" >> $GITHUB_OUTPUT ;; + 3.11) echo "py311=$SIZE" >> $GITHUB_OUTPUT ;; + 3.12) echo "py312=$SIZE" >> $GITHUB_OUTPUT ;; + 3.13) echo "py313=$SIZE" >> $GITHUB_OUTPUT ;; + esac + + comment-on-pr: + if: github.event_name == 'pull_request' && github.base_ref == 'master' + needs: measure-venv + runs-on: ubuntu-latest + steps: + - name: Post venv size summary to PR + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const sizes = { + "3.9": "${{ needs.measure-venv.outputs.py39 || 'N/A' }}", + "3.10": "${{ needs.measure-venv.outputs.py310 || 'N/A' }}", + "3.11": "${{ needs.measure-venv.outputs.py311 || 'N/A' }}", + "3.12": "${{ needs.measure-venv.outputs.py312 || 'N/A' }}", + "3.13": "${{ needs.measure-venv.outputs.py313 || 'N/A' }}", + }; + + const body = [ + '**Bittensor SDK virtual environment sizes by Python version:**', + '', + '```' + ] + .concat(Object.entries(sizes).map(([v, s]) => `Python ${v}: ${s} MB`)) + .concat(['```']) + .join('\n'); + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body + }); diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3d282dbb2d..1118590448 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ jobs: name: Build Python distribution runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 @@ -69,4 +69,4 @@ jobs: uses: pypa/gh-action-pypi-publish@release/v1 with: verbose: true - print-hash: true \ No newline at end of file + print-hash: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d4ca830d5..69844e7def 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,44 @@ # Changelog +## 9.5.0 /2025-05-12 + +## What's Changed +* Release/9.4.0 by @ibraheem-abe in https://github.com/opentensor/bittensor/pull/2837 +* Update subnet units by @thewhaleking in https://github.com/opentensor/bittensor/pull/2838 +* Add `force_register_neuron` into MockSubtensor by @basfroman in https://github.com/opentensor/bittensor/pull/2839 +* Add `Monitor-Requirements-Size` workflow by @basfroman in https://github.com/opentensor/bittensor/pull/2842 +* Add `SelectiveMetagraph` interface into SDK by @basfroman in https://github.com/opentensor/bittensor/pull/2846 +* Update docs for unstake amount by @thewhaleking in https://github.com/opentensor/bittensor/pull/2845 +* Add one more attempt to e2e tests by @basfroman in https://github.com/opentensor/bittensor/pull/2849 +* Fix typos in test documentation and docstrings by @leopardracer in https://github.com/opentensor/bittensor/pull/2848 +* Add bittensor-drand==0.5.0 by @basfroman in https://github.com/opentensor/bittensor/pull/2835 +* Extend selective metagraph logic by @basfroman in https://github.com/opentensor/bittensor/pull/2852 +* fix: $BASH_ENV loading issue by @defitricks in https://github.com/opentensor/bittensor/pull/2851 +* Update github actions versions due to deprecation by @PixelPil0t1 in https://github.com/opentensor/bittensor/pull/2850 +* Add methods to Async/Subtensor class by @basfroman in https://github.com/opentensor/bittensor/pull/2854 +* Cleanup, refactoring, small fix, improvement by @basfroman in https://github.com/opentensor/bittensor/pull/2856 +* Add `period` argument to extrinsics calls by @basfroman in https://github.com/opentensor/bittensor/pull/2857 +* Add nightly run of e2e tests by @basfroman in https://github.com/opentensor/bittensor/pull/2858 +* `period=16` for fast blocks test by @basfroman in https://github.com/opentensor/bittensor/pull/2859 +* Fix some unittests warnings by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2774 +* docs: fix typos in `README.md` by @gap-editor in https://github.com/opentensor/bittensor/pull/2861 +* Introduce `SubtensorApi` interface for SDK by @basfroman in https://github.com/opentensor/bittensor/pull/2862 +* Adds `__all__` to easy_imports.py to get rid of all the #noqa stuff by @thewhaleking in https://github.com/opentensor/bittensor/pull/2863 +* `đŸĨŠs` to `staking` by @basfroman in https://github.com/opentensor/bittensor/pull/2864 +* Fix `get_next_epoch_start_block` by @basfroman in https://github.com/opentensor/bittensor/pull/2865 +* docs: fix dead link by @GarmashAlex in https://github.com/opentensor/bittensor/pull/2866 +* Add `non-fast-blocks` e2e tests each Saturday by @basfroman in https://github.com/opentensor/bittensor/pull/2860 +* Selective-metagraph -> MetagraphInfo by @basfroman in https://github.com/opentensor/bittensor/pull/2870 +* Improve e2e tests and fix bug in SubtensorApi by @basfroman in https://github.com/opentensor/bittensor/pull/2869 + +## New Contributors +* @defitricks made their first contribution in https://github.com/opentensor/bittensor/pull/2851 +* @PixelPil0t1 made their first contribution in https://github.com/opentensor/bittensor/pull/2850 +* @gap-editor made their first contribution in https://github.com/opentensor/bittensor/pull/2861 +* @GarmashAlex made their first contribution in https://github.com/opentensor/bittensor/pull/2866 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.4.0...v9.5.0 + ## 9.4.0 /2025-04-17 ## What's Changed diff --git a/README.md b/README.md index 953ee9e5e8..e401bea455 100644 --- a/README.md +++ b/README.md @@ -234,23 +234,29 @@ pytest tests/unit_tests - using a compiler based on the substrait code - using an already built docker image (docker runner) +#### Local environment variables: +- `LOCALNET_SH_PATH` - path to `localnet.sh` script in cloned subtensor repository (for legacy runner); +- `BUILD_BINARY` - (`=0` or `=1`) - used with `LOCALNET_SH_PATH` for build or not before start localnet node (for legacy runner); +- `USE_DOCKER` - (`=0` or `=1`) - used if you want to use specific runner to run e2e tests (for docker runner); +- `FAST_BLOCKS` - (`=0` or `=1`) - allows you to run a localnet node in fast or non-fast blocks mode (for both types of runers). + #### Using `docker runner` (default for now): - E2E tests with docker image do not require preliminary compilation - are executed very quickly - require docker installed in OS -Ho to use: +How to use: ```bash pytest tests/e2e_tests ``` -#### TUsing `legacy runner`: +#### Using `legacy runner`: - Will start compilation of the collected code in your subtensor repository - you must provide the `LOCALNET_SH_PATH` variable in the local environment with the path to the file `/scripts/localnet.sh` in the cloned repository within your OS - you can use the `BUILD_BINARY=0` variable, this will skip the copy step for each test. - you can use the `USE_DOCKER=0` variable, this will run tests using the "legacy runner", even if docker is installed in your OS -#### Ho to use: +#### How to use: Regular e2e tests run ```bash LOCALNET_SH_PATH=/path/to/your/localnet.sh pytest tests/e2e_tests diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 1bd3ce9820..1402e4c683 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -9,24 +9,25 @@ import numpy as np import scalecodec from async_substrate_interface import AsyncSubstrateInterface -from bittensor_commit_reveal import get_encrypted_commitment +from async_substrate_interface.substrate_addons import RetryAsyncSubstrate +from bittensor_drand import get_encrypted_commitment from bittensor_wallet.utils import SS58_FORMAT from numpy.typing import NDArray from scalecodec import GenericCall from bittensor.core.chain_data import ( DelegateInfo, - StakeInfo, + DynamicInfo, MetagraphInfo, NeuronInfoLite, NeuronInfo, ProposalVoteData, + StakeInfo, SubnetHyperparameters, SubnetIdentity, SubnetInfo, WeightCommitInfo, decode_account_id, - DynamicInfo, ) from bittensor.core.chain_data.chain_identity import ChainIdentity from bittensor.core.chain_data.delegate_info import DelegatedInfo @@ -57,12 +58,12 @@ publish_metadata, get_metadata, ) -from bittensor.core.extrinsics.asyncex.start_call import start_call_extrinsic from bittensor.core.extrinsics.asyncex.serving import serve_axon_extrinsic from bittensor.core.extrinsics.asyncex.staking import ( add_stake_extrinsic, add_stake_multiple_extrinsic, ) +from bittensor.core.extrinsics.asyncex.start_call import start_call_extrinsic from bittensor.core.extrinsics.asyncex.take import ( decrease_take_extrinsic, increase_take_extrinsic, @@ -97,7 +98,7 @@ check_and_convert_to_balance, ) from bittensor.utils.btlogging import logging -from bittensor.utils.weight_utils import generate_weight_hash +from bittensor.utils.weight_utils import generate_weight_hash, convert_uids_and_weights if TYPE_CHECKING: from async_substrate_interface.types import ScaleObj @@ -113,8 +114,10 @@ def __init__( self, network: Optional[str] = None, config: Optional["Config"] = None, - _mock: bool = False, log_verbose: bool = False, + fallback_chains: Optional[list[str]] = None, + retry_forever: bool = False, + _mock: bool = False, ): """ Initializes an instance of the AsyncSubtensor class. @@ -122,8 +125,10 @@ def __init__( Arguments: network (str): The network name or type to connect to. config (Optional[Config]): Configuration object for the AsyncSubtensor instance. - _mock: Whether this is a mock instance. Mainly just for use in testing. log_verbose (bool): Enables or disables verbose logging. + fallback_chains (list): List of fallback chains endpoints to use if no network is specified. Defaults to `None`. + retry_forever (bool): Whether to retry forever on connection errors. Defaults to `False`. + _mock: Whether this is a mock instance. Mainly just for use in testing. Raises: Any exceptions raised during the setup, configuration, or connection process. @@ -134,7 +139,6 @@ def __init__( self.chain_endpoint, self.network = AsyncSubtensor.setup_config( network, self._config ) - self._mock = _mock self.log_verbose = log_verbose self._check_and_log_network_settings() @@ -143,13 +147,8 @@ def __init__( f"Connecting to network: [blue]{self.network}[/blue], " f"chain_endpoint: [blue]{self.chain_endpoint}[/blue]..." ) - self.substrate = AsyncSubstrateInterface( - url=self.chain_endpoint, - ss58_format=SS58_FORMAT, - type_registry=TYPE_REGISTRY, - use_remote_preset=True, - chain_name="Bittensor", - _mock=_mock, + self.substrate = self._get_substrate( + fallback_chains=fallback_chains, retry_forever=retry_forever, _mock=_mock ) if self.log_verbose: logging.info( @@ -228,7 +227,7 @@ async def encode_params( call_definition: dict[str, list["ParamWithTypes"]], params: Union[list[Any], dict[str, Any]], ) -> str: - """Returns a hex encoded string of the params using their types.""" + """Returns a hex-encoded string of the params using their types.""" param_data = scalecodec.ScaleBytes(b"") for i, param in enumerate(call_definition["params"]): @@ -283,6 +282,42 @@ async def get_hyperparameter( return getattr(result, "value", result) + def _get_substrate( + self, + fallback_chains: Optional[list[str]] = None, + retry_forever: bool = False, + _mock: bool = False, + ) -> Union[AsyncSubstrateInterface, RetryAsyncSubstrate]: + """Creates the Substrate instance based on provided arguments. + + Arguments: + fallback_chains (list): List of fallback chains endpoints to use if no network is specified. Defaults to `None`. + retry_forever (bool): Whether to retry forever on connection errors. Defaults to `False`. + _mock: Whether this is a mock instance. Mainly just for use in testing. + + Returns: + the instance of the SubstrateInterface or RetrySyncSubstrate class. + """ + if fallback_chains or retry_forever: + return RetryAsyncSubstrate( + url=self.chain_endpoint, + fallback_chains=fallback_chains, + ss58_format=SS58_FORMAT, + type_registry=TYPE_REGISTRY, + retry_forever=retry_forever, + use_remote_preset=True, + chain_name="Bittensor", + _mock=_mock, + ) + return AsyncSubstrateInterface( + url=self.chain_endpoint, + ss58_format=SS58_FORMAT, + type_registry=TYPE_REGISTRY, + use_remote_preset=True, + chain_name="Bittensor", + _mock=_mock, + ) + # Subtensor queries =========================================================================================== async def query_constant( @@ -579,6 +614,35 @@ async def all_subnets( subnets = DynamicInfo.list_from_dicts(query.decode()) return subnets + async def blocks_since_last_step( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[int]: + """Returns number of blocks since the last epoch of the subnet. + + Arguments: + netuid (int): The unique identifier of the subnetwork. + block: the block number for this query. + block_hash: The hash of the blockchain block number for the query. Do not specify if using reuse_block or + block. + reuse_block: Whether to reuse the last-used blockchain block hash. Do not set if using block_hash or block. + + + Returns: + block number of the last step in the subnet. + """ + query = await self.query_subtensor( + name="BlocksSinceLastStep", + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + params=[netuid], + ) + return query.value if query is not None and hasattr(query, "value") else query + async def blocks_since_last_update(self, netuid: int, uid: int) -> Optional[int]: """ Returns the number of blocks since the last update for a specific UID in the subnetwork. @@ -636,7 +700,9 @@ async def bonds( return b_map - async def commit(self, wallet: "Wallet", netuid: int, data: str) -> bool: + async def commit( + self, wallet: "Wallet", netuid: int, data: str, period: Optional[int] = None + ) -> bool: """ Commits arbitrary data to the Bittensor network by publishing metadata. @@ -644,6 +710,12 @@ async def commit(self, wallet: "Wallet", netuid: int, data: str) -> bool: wallet (bittensor_wallet.Wallet): The wallet associated with the neuron committing the data. netuid (int): The unique identifier of the subnetwork. data (str): The data to be committed to the network. + 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. + + Return: + bool: `True` if the commit was successful, `False` otherwise. """ return await publish_metadata( subtensor=self, @@ -1715,11 +1787,16 @@ async def get_next_epoch_start_block( int: The block number at which the next epoch will start. """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - if not block_hash and reuse_block: - block_hash = self.substrate.last_block_hash - block = await self.substrate.get_block_number(block_hash=block_hash) - tempo = await self.tempo(netuid=netuid, block_hash=block_hash) - return (((block // tempo) + 1) * tempo) + 1 if tempo else None + blocks_since_last_step = await self.blocks_since_last_step( + netuid=netuid, block=block, block_hash=block_hash, reuse_block=reuse_block + ) + tempo = await self.tempo( + netuid=netuid, block=block, block_hash=block_hash, reuse_block=reuse_block + ) + + if block and blocks_since_last_step and tempo: + return block - blocks_since_last_step + tempo + 1 + return None async def get_owned_hotkeys( self, @@ -2383,6 +2460,12 @@ async def immunity_period( ) return None if call is None else int(call) + async def is_fast_blocks(self): + """Returns True if the node is running with fast blocks. False if not.""" + return ( + await self.query_constant("SubtensorModule", "DurationOfStartCall") + ).value == 10 + async def is_hotkey_delegate( self, hotkey_ss58: str, @@ -2806,6 +2889,7 @@ async def set_reveal_commitment( data: str, blocks_until_reveal: int = 360, block_time: Union[int, float] = 12, + period: Optional[int] = None, ) -> tuple[bool, int]: """ Commits arbitrary data to the Bittensor network by publishing metadata. @@ -2815,8 +2899,11 @@ async def set_reveal_commitment( netuid (int): The unique identifier of the subnetwork. data (str): The data to be committed to the network. blocks_until_reveal (int): The number of blocks from now after which the data will be revealed. Defaults to `360`. - Then amount of blocks in one epoch. + The number of blocks in one epoch. block_time (Union[int, float]): The number of seconds between each block. Defaults to `12`. + 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. Returns: bool: `True` if the commitment was successful, `False` otherwise. @@ -2837,6 +2924,7 @@ async def set_reveal_commitment( netuid=netuid, data_type=f"TimelockEncrypted", data=data_, + period=period, ), reveal_round async def subnet( @@ -2994,14 +3082,14 @@ async def wait_for_block(self, block: Optional[int] = None): waits for the next block. Args: - block (Optional[int]): The block number to wait for. If None, waits for next block. + block (Optional[int]): The block number to wait for. If None, waits for the next block. Returns: bool: True if the target block was reached, False if timeout occurred. Example: await subtensor.wait_for_block() # Waits for next block - await subtensor.wait_for_block(block=1234) # Waits for specific block + await subtensor.wait_for_block(block=1234) # Waits for a specific block """ async def handler(block_data: dict): @@ -3010,6 +3098,7 @@ async def handler(block_data: dict): ) if block_data["header"]["number"] >= target_block: return True + return None current_block = await self.substrate.get_block() current_block_hash = current_block.get("header", {}).get("hash") @@ -3121,6 +3210,46 @@ async def get_timestamp( ).value return datetime.fromtimestamp(unix / 1000, tz=timezone.utc) + async def get_subnet_owner_hotkey( + self, netuid: int, block: Optional[int] = None + ) -> Optional[str]: + """ + Retrieves the hotkey of the subnet owner for a given network UID. + + This function queries the subtensor network to fetch the hotkey of the owner of a subnet specified by its + netuid. If no data is found or the query fails, the function returns None. + + Arguments: + netuid: The network UID of the subnet to fetch the owner's hotkey for. + block: The specific block number to query the data from. + + Returns: + The hotkey of the subnet owner if available; None otherwise. + """ + return await self.query_subtensor( + name="SubnetOwnerHotkey", params=[netuid], block=block + ) + + async def get_subnet_validator_permits( + self, netuid: int, block: Optional[int] = None + ) -> Optional[list[bool]]: + """ + Retrieves the list of validator permits for a given subnet as boolean values. + + Arguments: + netuid: The unique identifier of the subnetwork. + block: The blockchain block number for the query. + + Returns: + A list of boolean values representing validator permits, or None if not available. + """ + query = await self.query_subtensor( + name="ValidatorPermit", + params=[netuid], + block=block, + ) + return query.value if query is not None and hasattr(query, "value") else query + # Extrinsics helper ================================================================================================ async def sign_and_send_extrinsic( @@ -3144,10 +3273,19 @@ async def sign_and_send_extrinsic( wait_for_inclusion (bool): whether to wait until the extrinsic call is included on the chain wait_for_finalization (bool): whether to wait until the extrinsic call is finalized on the chain sign_with: the wallet's keypair to use for the signing. Options are "coldkey", "hotkey", "coldkeypub" - raise_error: raises relevant exception rather than returning `False` if unsuccessful. + use_nonce: unique identifier for the transaction related with hot/coldkey. + 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. + nonce_key: the type on nonce to use. Options are "hotkey" or "coldkey". + nonce_key: the type on nonce to use. Options are "hotkey", "coldkey", or "coldkeypub". + raise_error: raises a relevant exception rather than returning `False` if unsuccessful. Returns: (success, error message) + + Raises: + SubstrateRequestException: Substrate request exception. """ possible_keys = ("coldkey", "hotkey", "coldkeypub") if sign_with not in possible_keys: @@ -3177,7 +3315,9 @@ async def sign_and_send_extrinsic( ) # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: - return True, "" + message = "Not waiting for finalization or inclusion." + logging.debug(f"{message}. Extrinsic: {extrinsic}") + return True, message if await response.is_success: return True, "" @@ -3206,6 +3346,7 @@ async def add_stake( safe_staking: bool = False, allow_partial_stake: bool = False, rate_tolerance: float = 0.005, + period: Optional[int] = None, ) -> bool: """ Adds the specified amount of stake to a neuron identified by the hotkey ``SS58`` address. @@ -3226,6 +3367,9 @@ async def add_stake( exceed the threshold. Default is False. rate_tolerance (float): The maximum allowed price change ratio when staking. For example, 0.005 = 0.5% maximum price increase. Only used when safe_staking is True. Default is 0.005. + 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. Returns: bool: ``True`` if the staking is successful, False otherwise. @@ -3246,6 +3390,7 @@ async def add_stake( safe_staking=safe_staking, allow_partial_stake=allow_partial_stake, rate_tolerance=rate_tolerance, + period=period, ) async def add_stake_multiple( @@ -3291,6 +3436,7 @@ async def burned_register( netuid: int, wait_for_inclusion: bool = False, wait_for_finalization: bool = True, + period: Optional[int] = None, ) -> bool: """ Registers a neuron on the Bittensor network by recycling TAO. This method of registration involves recycling @@ -3303,6 +3449,9 @@ async def burned_register( `False`. wait_for_finalization (bool, optional): Waits for the transaction to be finalized on the blockchain. Defaults to `True`. + 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. Returns: bool: ``True`` if the registration is successful, False otherwise. @@ -3314,6 +3463,7 @@ async def burned_register( wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) return await burned_register_extrinsic( @@ -3322,6 +3472,7 @@ async def burned_register( netuid=netuid, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) async def commit_weights( @@ -3335,6 +3486,7 @@ async def commit_weights( wait_for_inclusion: bool = False, wait_for_finalization: bool = False, max_retries: int = 5, + period: Optional[int] = 16, ) -> tuple[bool, str]: """ Commits a hash of the neuron's weights to the Bittensor blockchain using the provided wallet. @@ -3347,15 +3499,19 @@ async def commit_weights( uids (np.ndarray): NumPy array of neuron UIDs for which weights are being committed. weights (np.ndarray): NumPy array of weight values corresponding to each UID. version_key (int): Version key for compatibility with the network. Default is ``int representation of - Bittensor version.``. + a Bittensor version.``. wait_for_inclusion (bool): Waits for the transaction to be included in a block. Default is ``False``. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Default is ``False``. max_retries (int): The number of maximum attempts to commit weights. Default is ``5``. + 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. Returns: - tuple[bool, str]: ``True`` if the weight commitment is successful, False otherwise. And `msg`, a string - value describing the success or potential error. + tuple[bool, str]: + `True` if the weight commitment is successful, False otherwise. + `msg` is a string value describing the success or potential error. This function allows neurons to create a tamper-proof record of their weight distribution at a specific point in time, enhancing transparency and accountability within the Bittensor network. @@ -3365,8 +3521,9 @@ async def commit_weights( message = "No attempt made. Perhaps it is too soon to commit weights!" logging.info( - f"Committing weights with params: netuid={netuid}, uids={uids}, weights={weights}, " - f"version_key={version_key}" + f"Committing weights with params: " + f"netuid=[blue]{netuid}[/blue], uids=[blue]{uids}[/blue], weights=[blue]{weights}[/blue], " + f"version_key=[blue]{version_key}[/blue]" ) # Generate the hash of the weights @@ -3388,12 +3545,12 @@ async def commit_weights( commit_hash=commit_hash, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) if success: break except Exception as e: logging.error(f"Error committing weights: {e}") - finally: retries += 1 return success, message @@ -3408,6 +3565,7 @@ async def move_stake( amount: Balance, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + period: Optional[int] = None, ) -> bool: """ Moves stake to a different hotkey and/or subnet. @@ -3421,6 +3579,9 @@ async def move_stake( amount (Balance): Amount of stake to move. wait_for_inclusion (bool): Waits for the transaction to be included in a block. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + 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. Returns: success (bool): True if the stake movement was successful. @@ -3436,6 +3597,7 @@ async def move_stake( amount=amount, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) async def register( @@ -3452,6 +3614,7 @@ async def register( num_processes: Optional[int] = None, update_interval: Optional[int] = None, log_verbose: bool = False, + period: Optional[int] = None, ): """ Registers a neuron on the Bittensor network using the provided wallet. @@ -3474,6 +3637,9 @@ async def register( num_processes (Optional[int]): The number of processes to use to register. Default to `None`. update_interval (Optional[int]): The number of nonces to solve between updates. Default to `None`. log_verbose (bool): If ``true``, the registration process will log more information. Default to `False`. + 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. Returns: bool: ``True`` if the registration is successful, False otherwise. @@ -3495,6 +3661,7 @@ async def register( dev_id=dev_id, output_in_place=output_in_place, log_verbose=log_verbose, + period=period, ) async def register_subnet( @@ -3502,6 +3669,7 @@ async def register_subnet( wallet: "Wallet", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, + period: Optional[int] = None, ) -> bool: """ Registers a new subnetwork on the Bittensor network. @@ -3512,6 +3680,9 @@ async def register_subnet( false if the extrinsic fails to enter the block within the timeout. Default is False. wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning true, or returns false if the extrinsic fails to be finalized within the timeout. Default is True. + 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. Returns: bool: True if the subnet registration was successful, False otherwise. @@ -3522,6 +3693,7 @@ async def register_subnet( wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) async def reveal_weights( @@ -3535,6 +3707,7 @@ async def reveal_weights( wait_for_inclusion: bool = False, wait_for_finalization: bool = False, max_retries: int = 5, + period: Optional[int] = None, ) -> tuple[bool, str]: """ Reveals the weights for a specific subnet on the Bittensor blockchain using the provided wallet. @@ -3547,11 +3720,14 @@ async def reveal_weights( weights (np.ndarray): NumPy array of weight values corresponding to each UID. salt (np.ndarray): NumPy array of salt values corresponding to the hash function. version_key (int): Version key for compatibility with the network. Default is ``int representation of - Bittensor version``. + the Bittensor version``. wait_for_inclusion (bool): Waits for the transaction to be included in a block. Default is ``False``. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Default is ``False``. max_retries (int): The number of maximum attempts to reveal weights. Default is ``5``. + 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. Returns: tuple[bool, str]: ``True`` if the weight revelation is successful, False otherwise. And `msg`, a string @@ -3576,32 +3752,37 @@ async def reveal_weights( version_key=version_key, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) if success: break except Exception as e: logging.error(f"Error revealing weights: {e}") - finally: retries += 1 return success, message + # TODO: remove `block_hash` argument async def root_register( self, wallet: "Wallet", block_hash: Optional[str] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, + period: Optional[int] = None, ) -> bool: """ Register neuron by recycling some TAO. Arguments: wallet (bittensor_wallet.Wallet): Bittensor wallet instance. - block_hash (Optional[str]): The hash of the blockchain block for the query. + block_hash (Optional[str]): This argument will be removed in Bittensor v10 wait_for_inclusion (bool): Waits for the transaction to be included in a block. Default is ``False``. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Default is ``False``. + 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. Returns: `True` if registration was successful, otherwise `False`. @@ -3612,6 +3793,7 @@ async def root_register( wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) async def root_set_weights( @@ -3622,9 +3804,10 @@ async def root_set_weights( version_key: int = 0, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, + period: Optional[int] = None, ) -> bool: """ - Set weights for root network. + Set weights for the root network. Arguments: wallet (bittensor_wallet.Wallet): bittensor wallet instance. @@ -3635,12 +3818,14 @@ async def root_set_weights( ``False``. wait_for_finalization (bool, optional): Waits for the transaction to be finalized on the blockchain. Defaults to ``False``. + 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. Returns: `True` if the setting of weights is successful, `False` otherwise. """ - netuids_ = np.array(netuids, dtype=np.int64) - weights_ = np.array(weights, dtype=np.float32) + netuids_, weights_ = convert_uids_and_weights(netuids, weights) logging.info(f"Setting weights in network: [blue]{self.network}[/blue]") # Run the set weights operation. return await set_root_weights_extrinsic( @@ -3651,6 +3836,7 @@ async def root_set_weights( version_key=version_key, wait_for_finalization=wait_for_finalization, wait_for_inclusion=wait_for_inclusion, + period=period, ) async def set_children( @@ -3662,9 +3848,10 @@ async def set_children( wait_for_inclusion: bool = True, wait_for_finalization: bool = True, raise_error: bool = False, + period: Optional[int] = None, ) -> tuple[bool, str]: """ - Allows a coldkey to set children keys. + Allows a coldkey to set children-keys. Arguments: wallet (bittensor_wallet.Wallet): bittensor wallet instance. @@ -3673,7 +3860,10 @@ async def set_children( children (list[tuple[float, str]]): A list of children with their proportions. wait_for_inclusion (bool): Waits for the transaction to be included in a block. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. - raise_error: Raises relevant exception rather than returning `False` if unsuccessful. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + 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. Returns: tuple[bool, str]: A tuple where the first element is a boolean indicating success or failure of the @@ -3720,6 +3910,7 @@ async def set_children( wait_for_inclusion, wait_for_finalization, raise_error=raise_error, + period=period, ) async def set_delegate_take( @@ -3730,6 +3921,7 @@ async def set_delegate_take( wait_for_inclusion: bool = True, wait_for_finalization: bool = True, raise_error: bool = False, + period: Optional[int] = None, ) -> tuple[bool, str]: """ Sets the delegate 'take' percentage for a neuron identified by its hotkey. @@ -3741,7 +3933,10 @@ async def set_delegate_take( take (float): Percentage reward for the delegate. wait_for_inclusion (bool): Waits for the transaction to be included in a block. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. - raise_error: Raises relevant exception rather than returning `False` if unsuccessful. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + 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. Returns: tuple[bool, str]: A tuple where the first element is a boolean indicating success or failure of the @@ -3751,8 +3946,8 @@ async def set_delegate_take( DelegateTakeTooHigh: Delegate take is too high. DelegateTakeTooLow: Delegate take is too low. DelegateTxRateLimitExceeded: A transactor exceeded the rate limit for delegate transaction. - HotKeyAccountNotExists: The hotkey does not exists. - NonAssociatedColdKey: Request to stake, unstake or subscribe is made by a coldkey that is not associated with the hotkey account. + HotKeyAccountNotExists: The hotkey does not exist. + NonAssociatedColdKey: Request to stake, unstake, or subscribe is made by a coldkey that is not associated with the hotkey account. bittensor_wallet.errors.PasswordError: Decryption failed or wrong password for decryption provided. bittensor_wallet.errors.KeyFileError: Failed to decode keyfile data. @@ -3781,6 +3976,7 @@ async def set_delegate_take( wait_for_finalization=wait_for_finalization, wait_for_inclusion=wait_for_inclusion, raise_error=raise_error, + period=period, ) else: success, error = await decrease_take_extrinsic( @@ -3791,6 +3987,7 @@ async def set_delegate_take( wait_for_finalization=wait_for_finalization, wait_for_inclusion=wait_for_inclusion, raise_error=raise_error, + period=period, ) if success: @@ -3805,6 +4002,7 @@ async def set_subnet_identity( subnet_identity: SubnetIdentity, wait_for_inclusion: bool = False, wait_for_finalization: bool = True, + period: Optional[int] = None, ) -> tuple[bool, str]: """ Sets the identity of a subnet for a specific wallet and network. @@ -3816,6 +4014,9 @@ async def set_subnet_identity( repository, contact, URL, discord, description, and any additional metadata. wait_for_inclusion (bool): Indicates if the function should wait for the transaction to be included in the block. wait_for_finalization (bool): Indicates if the function should wait for the transaction to reach finalization. + 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. Returns: tuple[bool, str]: A tuple where the first element is a boolean indicating success or failure of the @@ -3834,6 +4035,7 @@ async def set_subnet_identity( additional=subnet_identity.additional, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) async def set_weights( @@ -3847,7 +4049,7 @@ async def set_weights( wait_for_finalization: bool = False, max_retries: int = 5, block_time: float = 12.0, - period: int = 5, + period: Optional[int] = 8, ): """ Sets the inter-neuronal weights for the specified neuron. This process involves specifying the influence or @@ -3862,13 +4064,15 @@ async def set_weights( weights (Union[NDArray[np.float32], torch.FloatTensor, list]): The corresponding weights to be set for each UID. version_key (int): Version key for compatibility with the network. Default is int representation of - Bittensor version. + the Bittensor version. wait_for_inclusion (bool): Waits for the transaction to be included in a block. Default is ``False``. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Default is ``False``. max_retries (int): The number of maximum attempts to set weights. Default is ``5``. - block_time (float): The amount of seconds for block duration. Default is 12.0 seconds. - period (int, optional): The period in seconds to wait for extrinsic inclusion or finalization. Defaults to 5. + block_time (float): The number of seconds for block duration. Default is 12.0 seconds. + 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. Default is 16. Returns: tuple[bool, str]: ``True`` if the setting of weights is successful, False otherwise. And `msg`, a string @@ -3887,6 +4091,7 @@ async def _blocks_weight_limit() -> bool: retries = 0 success = False + message = "No attempt made. Perhaps it is too soon to set weights!" if ( uid := await self.get_uid_for_hotkey_on_subnet( wallet.hotkey.ss58_address, netuid @@ -3899,7 +4104,7 @@ async def _blocks_weight_limit() -> bool: if (await self.commit_reveal_enabled(netuid=netuid)) is True: # go with `commit reveal v3` extrinsic - message = "No attempt made. Perhaps it is too soon to commit weights!" + while ( retries < max_retries and success is False @@ -3918,12 +4123,13 @@ async def _blocks_weight_limit() -> bool: wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, block_time=block_time, + period=period, ) retries += 1 return success, message else: # go with classic `set weights extrinsic` - message = "No attempt made. Perhaps it is too soon to set weights!" + while ( retries < max_retries and success is False @@ -3932,7 +4138,7 @@ async def _blocks_weight_limit() -> bool: try: logging.info( f"Setting weights for subnet #[blue]{netuid}[/blue]. " - f"Attempt [blue]{retries + 1} of {max_retries}[/blue]." + f"Attempt [blue]{retries + 1}[/blue] of [green]{max_retries}[/green]." ) success, message = await set_weights_extrinsic( subtensor=self, @@ -3947,7 +4153,6 @@ async def _blocks_weight_limit() -> bool: ) except Exception as e: logging.error(f"Error setting weights: {e}") - finally: retries += 1 return success, message @@ -3959,6 +4164,7 @@ async def serve_axon( wait_for_inclusion: bool = False, wait_for_finalization: bool = True, certificate: Optional[Certificate] = None, + period: Optional[int] = None, ) -> bool: """ Registers an ``Axon`` serving endpoint on the Bittensor network for a specific neuron. This function is used to @@ -3972,6 +4178,9 @@ async def serve_axon( ``True``. certificate (bittensor.utils.Certificate): Certificate to use for TLS. If ``None``, no TLS will be used. Defaults to ``None``. + 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. Returns: bool: ``True`` if the Axon serve registration is successful, False otherwise. @@ -3986,6 +4195,7 @@ async def serve_axon( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, certificate=certificate, + period=period, ) async def start_call( @@ -3994,6 +4204,7 @@ async def start_call( netuid: int, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + period: Optional[int] = None, ) -> tuple[bool, str]: """ Submits a start_call extrinsic to the blockchain, to trigger the start call process for a subnet (used to start a @@ -4004,6 +4215,9 @@ async def start_call( netuid (int): The UID of the target subnet for which the call is being initiated. wait_for_inclusion (bool, optional): Whether to wait for the extrinsic to be included in a block. Defaults to True. wait_for_finalization (bool, optional): Whether to wait for finalization of the extrinsic. Defaults to False. + 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. Returns: Tuple[bool, str]: @@ -4016,6 +4230,7 @@ async def start_call( netuid=netuid, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) async def swap_stake( @@ -4030,6 +4245,7 @@ async def swap_stake( safe_staking: bool = False, allow_partial_stake: bool = False, rate_tolerance: float = 0.005, + period: Optional[int] = None, ) -> bool: """ Moves stake between subnets while keeping the same coldkey-hotkey pair ownership. @@ -4052,6 +4268,9 @@ async def swap_stake( rate_tolerance (float): The maximum allowed increase in the price ratio between subnets (origin_price/destination_price). For example, 0.005 = 0.5% maximum increase. Only used when safe_staking is True. Default is 0.005. + 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. Returns: success (bool): True if the extrinsic was successful. @@ -4076,6 +4295,49 @@ async def swap_stake( safe_staking=safe_staking, allow_partial_stake=allow_partial_stake, rate_tolerance=rate_tolerance, + period=period, + ) + + async def transfer( + self, + wallet: "Wallet", + dest: str, + amount: Balance, + transfer_all: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + keep_alive: bool = True, + period: Optional[int] = None, + ) -> bool: + """ + Transfer token of amount to destination. + + Arguments: + wallet (bittensor_wallet.Wallet): Source wallet for the transfer. + dest (str): Destination address for the transfer. + amount (float): Number of tokens to transfer. + transfer_all (bool): Flag to transfer all tokens. Default is ``False``. + wait_for_inclusion (bool): Waits for the transaction to be included in a block. Default is ``True``. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Default is + ``False``. + keep_alive (bool): Flag to keep the connection alive. Default is ``True``. + 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. + Returns: + `True` if the transferring was successful, otherwise `False`. + """ + amount = check_and_convert_to_balance(amount) + return await transfer_extrinsic( + subtensor=self, + wallet=wallet, + dest=dest, + amount=amount, + transfer_all=transfer_all, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + keep_alive=keep_alive, + period=period, ) async def transfer_stake( @@ -4088,6 +4350,7 @@ async def transfer_stake( amount: Balance, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + period: Optional[int] = None, ) -> bool: """ Transfers stake from one subnet to another while changing the coldkey owner. @@ -4101,6 +4364,9 @@ async def transfer_stake( amount (Balance): Amount to transfer. wait_for_inclusion (bool): If true, waits for inclusion before returning. wait_for_finalization (bool): If true, waits for finalization before returning. + 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. Returns: success (bool): True if the transfer was successful. @@ -4116,44 +4382,7 @@ async def transfer_stake( amount=amount, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, - ) - - async def transfer( - self, - wallet: "Wallet", - dest: str, - amount: Balance, - transfer_all: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - keep_alive: bool = True, - ) -> bool: - """ - Transfer token of amount to destination. - - Arguments: - wallet (bittensor_wallet.Wallet): Source wallet for the transfer. - dest (str): Destination address for the transfer. - amount (float): Amount of tokens to transfer. - transfer_all (bool): Flag to transfer all tokens. Default is ``False``. - wait_for_inclusion (bool): Waits for the transaction to be included in a block. Default is ``True``. - wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Default is - ``False``. - keep_alive (bool): Flag to keep the connection alive. Default is ``True``. - - Returns: - `True` if the transferring was successful, otherwise `False`. - """ - amount = check_and_convert_to_balance(amount) - return await transfer_extrinsic( - subtensor=self, - wallet=wallet, - dest=dest, - amount=amount, - transfer_all=transfer_all, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - keep_alive=keep_alive, + period=period, ) async def unstake( @@ -4167,6 +4396,7 @@ async def unstake( safe_staking: bool = False, allow_partial_stake: bool = False, rate_tolerance: float = 0.005, + period: Optional[int] = None, ) -> bool: """ Removes a specified amount of stake from a single hotkey account. This function is critical for adjusting @@ -4177,7 +4407,7 @@ async def unstake( removed. hotkey_ss58 (Optional[str]): The ``SS58`` address of the hotkey account to unstake from. netuid (Optional[int]): The unique identifier of the subnet. - amount (Balance): The amount of TAO to unstake. If not specified, unstakes all. + amount (Balance): The amount of alpha to unstake. If not specified, unstakes all. wait_for_inclusion (bool): Waits for the transaction to be included in a block. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. safe_staking (bool): If true, enables price safety checks to protect against fluctuating prices. The unstake @@ -4187,6 +4417,9 @@ async def unstake( exceed the threshold. Default is False. rate_tolerance (float): The maximum allowed price change ratio when unstaking. For example, 0.005 = 0.5% maximum price decrease. Only used when safe_staking is True. Default is 0.005. + 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. Returns: bool: ``True`` if the unstaking process is successful, False otherwise. @@ -4206,6 +4439,7 @@ async def unstake( safe_staking=safe_staking, allow_partial_stake=allow_partial_stake, rate_tolerance=rate_tolerance, + period=period, ) async def unstake_multiple( @@ -4216,6 +4450,7 @@ async def unstake_multiple( amounts: Optional[list[Balance]] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + period: Optional[int] = None, ) -> bool: """ Performs batch unstaking from multiple hotkey accounts, allowing a neuron to reduce its staked amounts @@ -4230,6 +4465,9 @@ async def unstake_multiple( unstakes all available stakes. wait_for_inclusion (bool): Waits for the transaction to be included in a block. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + 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. Returns: bool: ``True`` if the batch unstaking is successful, False otherwise. @@ -4245,6 +4483,7 @@ async def unstake_multiple( amounts=amounts, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) diff --git a/bittensor/core/chain_data/__init__.py b/bittensor/core/chain_data/__init__.py index 03b5fbe251..9e1852e1d5 100644 --- a/bittensor/core/chain_data/__init__.py +++ b/bittensor/core/chain_data/__init__.py @@ -16,6 +16,7 @@ MetagraphInfoEmissions, MetagraphInfoPool, MetagraphInfoParams, + SelectiveMetagraphIndex, ) from .neuron_info import NeuronInfo from .neuron_info_lite import NeuronInfoLite @@ -50,6 +51,7 @@ ProposalCallData, ProposalVoteData, ScheduledColdkeySwapInfo, + SelectiveMetagraphIndex, StakeInfo, SubnetHyperparameters, SubnetIdentity, diff --git a/bittensor/core/chain_data/metagraph_info.py b/bittensor/core/chain_data/metagraph_info.py index 422a0607b3..3ad189c6fd 100644 --- a/bittensor/core/chain_data/metagraph_info.py +++ b/bittensor/core/chain_data/metagraph_info.py @@ -1,3 +1,5 @@ +from enum import Enum + from dataclasses import dataclass from typing import Optional, Union @@ -12,8 +14,10 @@ # to balance with unit (just shortcut) -def _tbwu(val: int, netuid: Optional[int] = 0) -> Balance: +def _tbwu(val: Optional[int], netuid: Optional[int] = 0) -> Optional[Balance]: """Returns a Balance object from a value and unit.""" + if val is None: + return None return Balance.from_rao(val, netuid) @@ -49,8 +53,8 @@ class MetagraphInfo(InfoBase): network_registered_at: int # Keys for owner. - owner_hotkey: str # hotkey - owner_coldkey: str # coldkey + owner_hotkey: Optional[str] # hotkey + owner_coldkey: Optional[str] # coldkey # Tempo terms. block: int # block at call. @@ -146,9 +150,20 @@ def _from_dict(cls, decoded: dict) -> "MetagraphInfo": _netuid = decoded["netuid"] # Name and symbol - decoded.update({"name": bytes(decoded.get("name")).decode()}) - decoded.update({"symbol": bytes(decoded.get("symbol")).decode()}) - for key in ["identities", "identity"]: + if name := decoded.get("name"): + decoded.update({"name": bytes(name).decode()}) + + if symbol := decoded.get("symbol"): + decoded.update({"symbol": bytes(symbol).decode()}) + + ii_list = [] + if decoded.get("identity") is not None: + ii_list.append("identity") + + if decoded.get("identities") is not None: + ii_list.append("identities") + + for key in ii_list: raw_data = decoded.get(key) processed = process_nested(raw_data, _chr_str) decoded.update({key: processed}) @@ -162,8 +177,16 @@ def _from_dict(cls, decoded: dict) -> "MetagraphInfo": identity=decoded["identity"], network_registered_at=decoded["network_registered_at"], # Keys for owner. - owner_hotkey=decoded["owner_hotkey"], - owner_coldkey=decoded["owner_coldkey"], + owner_hotkey=( + decode_account_id(decoded["owner_hotkey"][0]) + if decoded.get("owner_hotkey") is not None + else None + ), + owner_coldkey=( + decode_account_id(decoded["owner_coldkey"][0]) + if decoded.get("owner_coldkey") is not None + else None + ), # Tempo terms. block=decoded["block"], tempo=decoded["tempo"], @@ -180,15 +203,25 @@ def _from_dict(cls, decoded: dict) -> "MetagraphInfo": pending_alpha_emission=_tbwu(decoded["pending_alpha_emission"], _netuid), pending_root_emission=_tbwu(decoded["pending_root_emission"]), subnet_volume=_tbwu(decoded["subnet_volume"], _netuid), - moving_price=Balance.from_tao( - fixed_to_float(decoded.get("moving_price"), 32) + moving_price=( + Balance.from_tao(fixed_to_float(decoded.get("moving_price"), 32)) + if decoded.get("moving_price") is not None + else None ), # Hparams for epoch rho=decoded["rho"], kappa=decoded["kappa"], # Validator params - min_allowed_weights=u16tf(decoded["min_allowed_weights"]), - max_weights_limit=u16tf(decoded["max_weights_limit"]), + min_allowed_weights=( + u16tf(decoded["min_allowed_weights"]) + if decoded.get("min_allowed_weights") is not None + else None + ), + max_weights_limit=( + u16tf(decoded["max_weights_limit"]) + if decoded["max_weights_limit"] is not None + else None + ), weights_version=decoded["weights_version"], weights_rate_limit=decoded["weights_rate_limit"], activity_cutoff=decoded["activity_cutoff"], @@ -197,15 +230,31 @@ def _from_dict(cls, decoded: dict) -> "MetagraphInfo": num_uids=decoded["num_uids"], max_uids=decoded["max_uids"], burn=_tbwu(decoded["burn"]), - difficulty=u64tf(decoded["difficulty"]), + difficulty=( + u64tf(decoded["difficulty"]) + if decoded["difficulty"] is not None + else None + ), registration_allowed=decoded["registration_allowed"], pow_registration_allowed=decoded["pow_registration_allowed"], immunity_period=decoded["immunity_period"], - min_difficulty=u64tf(decoded["min_difficulty"]), - max_difficulty=u64tf(decoded["max_difficulty"]), + min_difficulty=( + u64tf(decoded["min_difficulty"]) + if decoded["min_difficulty"] is not None + else None + ), + max_difficulty=( + u64tf(decoded["max_difficulty"]) + if decoded["max_difficulty"] is not None + else None + ), min_burn=_tbwu(decoded["min_burn"]), max_burn=_tbwu(decoded["max_burn"]), - adjustment_alpha=u64tf(decoded["adjustment_alpha"]), + adjustment_alpha=( + u64tf(decoded["adjustment_alpha"]) + if decoded["adjustment_alpha"] is not None + else None + ), adjustment_interval=decoded["adjustment_interval"], target_regs_per_interval=decoded["target_regs_per_interval"], max_regs_per_block=decoded["max_regs_per_block"], @@ -215,40 +264,108 @@ def _from_dict(cls, decoded: dict) -> "MetagraphInfo": commit_reveal_period=decoded["commit_reveal_period"], # Bonds liquid_alpha_enabled=decoded["liquid_alpha_enabled"], - alpha_high=u16tf(decoded["alpha_high"]), - alpha_low=u16tf(decoded["alpha_low"]), - bonds_moving_avg=u64tf(decoded["bonds_moving_avg"]), + alpha_high=( + u16tf(decoded["alpha_high"]) + if decoded["alpha_high"] is not None + else None + ), + alpha_low=( + u16tf(decoded["alpha_low"]) + if decoded["alpha_low"] is not None + else None + ), + bonds_moving_avg=( + u64tf(decoded["bonds_moving_avg"]) + if decoded["bonds_moving_avg"] is not None + else None + ), # Metagraph info. - hotkeys=[decode_account_id(ck) for ck in decoded.get("hotkeys", [])], - coldkeys=[decode_account_id(hk) for hk in decoded.get("coldkeys", [])], + hotkeys=( + [decode_account_id(ck) for ck in decoded.get("hotkeys", [])] + if decoded.get("hotkeys") is not None + else None + ), + coldkeys=( + [decode_account_id(hk) for hk in decoded.get("coldkeys", [])] + if decoded.get("coldkeys") is not None + else None + ), identities=decoded["identities"], axons=decoded.get("axons", []), active=decoded["active"], validator_permit=decoded["validator_permit"], - pruning_score=[u16tf(ps) for ps in decoded.get("pruning_score", [])], + pruning_score=( + [u16tf(ps) for ps in decoded.get("pruning_score", [])] + if decoded.get("pruning_score") is not None + else None + ), last_update=decoded["last_update"], - emission=[_tbwu(em, _netuid) for em in decoded.get("emission", [])], - dividends=[u16tf(dv) for dv in decoded.get("dividends", [])], - incentives=[u16tf(ic) for ic in decoded.get("incentives", [])], - consensus=[u16tf(cs) for cs in decoded.get("consensus", [])], - trust=[u16tf(tr) for tr in decoded.get("trust", [])], - rank=[u16tf(rk) for rk in decoded.get("rank", [])], + emission=( + [_tbwu(em, _netuid) for em in decoded.get("emission", [])] + if decoded.get("emission") is not None + else None + ), + dividends=( + [u16tf(dv) for dv in decoded.get("dividends", [])] + if decoded.get("dividends") is not None + else None + ), + incentives=( + [u16tf(ic) for ic in decoded.get("incentives", [])] + if decoded.get("incentives") is not None + else None + ), + consensus=( + [u16tf(cs) for cs in decoded.get("consensus", [])] + if decoded.get("consensus") is not None + else None + ), + trust=( + [u16tf(tr) for tr in decoded.get("trust", [])] + if decoded.get("trust") is not None + else None + ), + rank=( + [u16tf(rk) for rk in decoded.get("rank", [])] + if decoded.get("rank") is not None + else None + ), block_at_registration=decoded["block_at_registration"], - alpha_stake=[_tbwu(ast, _netuid) for ast in decoded["alpha_stake"]], - tao_stake=[ - _tbwu(ts) * settings.ROOT_TAO_STAKE_WEIGHT - for ts in decoded["tao_stake"] - ], - total_stake=[_tbwu(ts, _netuid) for ts in decoded["total_stake"]], + alpha_stake=( + [_tbwu(ast, _netuid) for ast in decoded["alpha_stake"]] + if decoded.get("alpha_stake") is not None + else None + ), + tao_stake=( + [ + _tbwu(ts) * settings.ROOT_TAO_STAKE_WEIGHT + for ts in decoded["tao_stake"] + ] + if decoded.get("tao_stake") is not None + else None + ), + total_stake=( + [_tbwu(ts, _netuid) for ts in decoded["total_stake"]] + if decoded.get("total_stake") is not None + else None + ), # Dividend break down - tao_dividends_per_hotkey=[ - (decode_account_id(alpha[0]), _tbwu(alpha[1])) - for alpha in decoded["tao_dividends_per_hotkey"] - ], - alpha_dividends_per_hotkey=[ - (decode_account_id(adphk[0]), _tbwu(adphk[1], _netuid)) - for adphk in decoded["alpha_dividends_per_hotkey"] - ], + tao_dividends_per_hotkey=( + [ + (decode_account_id(alpha[0]), _tbwu(alpha[1])) + for alpha in decoded["tao_dividends_per_hotkey"] + ] + if decoded.get("tao_dividends_per_hotkey") is not None + else None + ), + alpha_dividends_per_hotkey=( + [ + (decode_account_id(adphk[0]), _tbwu(adphk[1], _netuid)) + for adphk in decoded["alpha_dividends_per_hotkey"] + ] + if decoded.get("alpha_dividends_per_hotkey") is not None + else None + ), ) @@ -306,3 +423,82 @@ class MetagraphInfoParams: tempo: int weights_rate_limit: int weights_version: int + + +class SelectiveMetagraphIndex(Enum): + Netuid = 0 + Name = 1 + Symbol = 2 + Identity = 3 + NetworkRegisteredAt = 4 + OwnerHotkey = 5 + OwnerColdkey = 6 + Block = 7 + Tempo = 8 + LastStep = 9 + BlocksSinceLastStep = 10 + SubnetEmission = 11 + AlphaIn = 12 + AlphaOut = 13 + TaoIn = 14 + AlphaOutEmission = 15 + AlphaInEmission = 16 + TaoInEmission = 17 + PendingAlphaEmission = 18 + PendingRootEmission = 19 + SubnetVolume = 20 + MovingPrice = 21 + Rho = 22 + Kappa = 23 + MinAllowedWeights = 24 + MaxWeightsLimit = 25 + WeightsVersion = 26 + WeightsRateLimit = 27 + ActivityCutoff = 28 + MaxValidators = 29 + NumUids = 30 + MaxUids = 31 + Burn = 32 + Difficulty = 33 + RegistrationAllowed = 34 + PowRegistrationAllowed = 35 + ImmunityPeriod = 36 + MinDifficulty = 37 + MaxDifficulty = 38 + MinBurn = 39 + MaxBurn = 40 + AdjustmentAlpha = 41 + AdjustmentInterval = 42 + TargetRegsPerInterval = 43 + MaxRegsPerBlock = 44 + ServingRateLimit = 45 + CommitRevealWeightsEnabled = 46 + CommitRevealPeriod = 47 + LiquidAlphaEnabled = 48 + AlphaHigh = 49 + AlphaLow = 50 + BondsMovingAvg = 51 + Hotkeys = 52 + Coldkeys = 53 + Identities = 54 + Axons = 55 + Active = 56 + ValidatorPermit = 57 + PruningScore = 58 + LastUpdate = 59 + Emission = 60 + Dividends = 61 + Incentives = 62 + Consensus = 63 + Trust = 64 + Rank = 65 + BlockAtRegistration = 66 + AlphaStake = 67 + TaoStake = 68 + TotalStake = 69 + TaoDividendsPerHotkey = 70 + AlphaDividendsPerHotkey = 71 + + @staticmethod + def all_indices() -> list[int]: + return [member.value for member in SelectiveMetagraphIndex] diff --git a/bittensor/core/extrinsics/asyncex/commit_reveal.py b/bittensor/core/extrinsics/asyncex/commit_reveal.py index d1b528e509..9c65ae8ea1 100644 --- a/bittensor/core/extrinsics/asyncex/commit_reveal.py +++ b/bittensor/core/extrinsics/asyncex/commit_reveal.py @@ -3,12 +3,12 @@ from typing import Optional, Union, TYPE_CHECKING import numpy as np -from bittensor_commit_reveal import get_encrypted_commit +from bittensor_drand import get_encrypted_commit from numpy.typing import NDArray from bittensor.core.settings import version_as_int from bittensor.utils.btlogging import logging -from bittensor.utils.weight_utils import convert_weights_and_uids_for_emit +from bittensor.utils.weight_utils import convert_and_normalize_weights_and_uids if TYPE_CHECKING: from bittensor_wallet import Wallet @@ -24,23 +24,26 @@ async def _do_commit_reveal_v3( reveal_round: int, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, -) -> tuple[bool, Optional[str]]: + period: Optional[int] = None, +) -> tuple[bool, str]: """ - Executes the commit-reveal phase 3 for a given netuid and commit, and optionally waits for extrinsic inclusion or - finalization. + Executes commit-reveal phase 3 for a given netuid and commit, and optionally waits for extrinsic inclusion or finalization. Arguments: subtensor: An instance of the AsyncSubtensor class. wallet: Wallet An instance of the Wallet class containing the user's keypair. netuid: int The network unique identifier. - commit bytes The commit data in bytes format. + commit: bytes The commit data in bytes format. reveal_round: int The round number for the reveal phase. 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 + 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. Returns: - A tuple where the first element is a boolean indicating success or failure, and the second element is an - optional string containing error message if any. + A tuple where the first element is a boolean indicating success or failure, and the second element is a + string containing an error message if any. """ logging.info( f"Committing weights hash [blue]{commit.hex()}[/blue] for subnet #[blue]{netuid}[/blue] with " @@ -57,7 +60,12 @@ async def _do_commit_reveal_v3( }, ) return await subtensor.sign_and_send_extrinsic( - call, wallet, wait_for_inclusion, wait_for_finalization, sign_with="hotkey" + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + sign_with="hotkey", + period=period, ) @@ -70,10 +78,11 @@ async def commit_reveal_v3_extrinsic( version_key: int = version_as_int, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, - block_time: float = 12.0, + block_time: Union[int, float] = 12.0, + period: Optional[int] = None, ) -> tuple[bool, str]: """ - Commits and reveals weights for given subtensor and wallet with provided uids and weights. + Commits and reveals weights for a given subtensor and wallet with provided uids and weights. Arguments: subtensor: The AsyncSubtensor instance. @@ -84,21 +93,17 @@ async def commit_reveal_v3_extrinsic( version_key: The version key to use for committing and revealing. Default is version_as_int. wait_for_inclusion: Whether to wait for the inclusion of the transaction. Default is False. wait_for_finalization: Whether to wait for the finalization of the transaction. Default is False. - block_time (float): The amount of seconds for block duration. Default is 12.0 seconds. + block_time (float): The number of seconds for block duration. Default is 12.0 seconds. + 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. Returns: tuple[bool, str]: A tuple where the first element is a boolean indicating success or failure, and the second element is a message associated with the result """ try: - # Convert uids and weights - if isinstance(uids, list): - uids = np.array(uids, dtype=np.int64) - if isinstance(weights, list): - weights = np.array(weights, dtype=np.float32) - - # Reformat and normalize. - uids, weights = convert_weights_and_uids_for_emit(uids, weights) + uids, weights = convert_and_normalize_weights_and_uids(uids, weights) current_block = await subtensor.substrate.get_block(None) subnet_hyperparameters = await subtensor.get_subnet_hyperparameters( @@ -127,6 +132,7 @@ async def commit_reveal_v3_extrinsic( reveal_round=reveal_round, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) if success is not True: diff --git a/bittensor/core/extrinsics/asyncex/move_stake.py b/bittensor/core/extrinsics/asyncex/move_stake.py index 6ab0b13028..8cfd5a2604 100644 --- a/bittensor/core/extrinsics/asyncex/move_stake.py +++ b/bittensor/core/extrinsics/asyncex/move_stake.py @@ -1,5 +1,5 @@ import asyncio -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging @@ -18,6 +18,7 @@ async def _get_stake_in_origin_and_dest( origin_netuid: int, destination_netuid: int, ) -> tuple[Balance, Balance]: + """Gets the current stake balances for both origin and destination addresses in their respective subnets.""" block_hash = await subtensor.substrate.get_chain_head() stake_in_origin, stake_in_destination = await asyncio.gather( subtensor.get_stake( @@ -46,6 +47,7 @@ async def transfer_stake_extrinsic( amount: Balance, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + period: Optional[int] = None, ) -> bool: """ Transfers stake from one coldkey to another in the Bittensor network. @@ -60,6 +62,9 @@ async def transfer_stake_extrinsic( amount (Balance): The amount of stake to transfer as a `Balance` object. wait_for_inclusion (bool): If True, waits for transaction inclusion in a block. Defaults to `True`. wait_for_finalization (bool): If True, waits for transaction finalization. Defaults to `False`. + 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. Returns: bool: True if the transfer was successful, False otherwise. @@ -77,7 +82,7 @@ async def transfer_stake_extrinsic( # Check sufficient stake stake_in_origin, stake_in_destination = await _get_stake_in_origin_and_dest( - subtensor, + subtensor=subtensor, origin_hotkey_ss58=hotkey_ss58, destination_hotkey_ss58=hotkey_ss58, origin_coldkey_ss58=wallet.coldkeypub.ss58_address, @@ -116,6 +121,7 @@ async def transfer_stake_extrinsic( wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) if success: @@ -126,7 +132,7 @@ async def transfer_stake_extrinsic( # Get updated stakes origin_stake, dest_stake = await _get_stake_in_origin_and_dest( - subtensor, + subtensor=subtensor, origin_hotkey_ss58=hotkey_ss58, destination_hotkey_ss58=hotkey_ss58, origin_coldkey_ss58=wallet.coldkeypub.ss58_address, @@ -163,6 +169,7 @@ async def swap_stake_extrinsic( safe_staking: bool = False, allow_partial_stake: bool = False, rate_tolerance: float = 0.005, + period: Optional[int] = None, ) -> bool: """ Swaps stake from one subnet to another for a given hotkey in the Bittensor network. @@ -178,7 +185,10 @@ async def swap_stake_extrinsic( wait_for_finalization (bool): If True, waits for transaction finalization. Defaults to False. safe_staking (bool): If true, enables price safety checks to protect against price impact. allow_partial_stake (bool): If true, allows partial stake swaps when the full amount would exceed the price tolerance. - rate_tolerance (float): Maximum allowed increase in price ratio (0.005 = 0.5%). + rate_tolerance (float): Maximum allowed increase in a price ratio (0.005 = 0.5%). + 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. Returns: bool: True if the swap was successful, False otherwise. @@ -195,7 +205,7 @@ async def swap_stake_extrinsic( # Check sufficient stake stake_in_origin, stake_in_destination = await _get_stake_in_origin_and_dest( - subtensor, + subtensor=subtensor, origin_hotkey_ss58=hotkey_ss58, destination_hotkey_ss58=hotkey_ss58, origin_coldkey_ss58=wallet.coldkeypub.ss58_address, @@ -259,6 +269,7 @@ async def swap_stake_extrinsic( wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) if success: @@ -309,6 +320,7 @@ async def move_stake_extrinsic( amount: Balance, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + period: Optional[int] = None, ) -> bool: """ Moves stake from one hotkey to another within subnets in the Bittensor network. @@ -323,6 +335,9 @@ async def move_stake_extrinsic( amount (Balance): The amount of stake to move as a `Balance` object. wait_for_inclusion (bool): If True, waits for transaction inclusion in a block. Defaults to True. wait_for_finalization (bool): If True, waits for transaction finalization. Defaults to False. + 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. Returns: bool: True if the move was successful, False otherwise. @@ -331,7 +346,7 @@ async def move_stake_extrinsic( # Check sufficient stake stake_in_origin, stake_in_destination = await _get_stake_in_origin_and_dest( - subtensor, + subtensor=subtensor, origin_hotkey_ss58=origin_hotkey, destination_hotkey_ss58=destination_hotkey, origin_coldkey_ss58=wallet.coldkeypub.ss58_address, @@ -369,6 +384,7 @@ async def move_stake_extrinsic( wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) if success: @@ -379,7 +395,7 @@ async def move_stake_extrinsic( # Get updated stakes origin_stake, dest_stake = await _get_stake_in_origin_and_dest( - subtensor, + subtensor=subtensor, origin_hotkey_ss58=origin_hotkey, destination_hotkey_ss58=destination_hotkey, origin_coldkey_ss58=wallet.coldkeypub.ss58_address, diff --git a/bittensor/core/extrinsics/asyncex/registration.py b/bittensor/core/extrinsics/asyncex/registration.py index cfdd78e6f5..8758169869 100644 --- a/bittensor/core/extrinsics/asyncex/registration.py +++ b/bittensor/core/extrinsics/asyncex/registration.py @@ -10,7 +10,7 @@ import asyncio from typing import Optional, Union, TYPE_CHECKING -from bittensor.utils import unlock_key, format_error_message +from bittensor.utils import unlock_key from bittensor.utils.btlogging import logging from bittensor.utils.registration import log_no_torch_error, create_pow_async, torch @@ -26,6 +26,7 @@ async def _do_burned_register( wallet: "Wallet", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, + period: Optional[int] = None, ) -> tuple[bool, str]: """ Performs a burned register extrinsic call to the Subtensor chain. @@ -38,6 +39,9 @@ async def _do_burned_register( wallet (bittensor_wallet.Wallet): The wallet to be registered. wait_for_inclusion (bool): Whether to wait for the transaction to be included in a block. Default is False. wait_for_finalization (bool): Whether to wait for the transaction to be finalized. Default is True. + 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. Returns: Tuple[bool, Optional[str]]: A tuple containing a boolean indicating success or failure, and an optional error @@ -58,6 +62,7 @@ async def _do_burned_register( wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) @@ -67,6 +72,7 @@ async def burned_register_extrinsic( netuid: int, wait_for_inclusion: bool = False, wait_for_finalization: bool = True, + period: Optional[int] = None, ) -> bool: """Registers the wallet to chain by recycling TAO. @@ -78,6 +84,9 @@ async def burned_register_extrinsic( 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 ``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout. + 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. Returns: success (bool): Flag is ``True`` if extrinsic was finalized or included in the block. If we did not wait for @@ -98,7 +107,7 @@ async def burned_register_extrinsic( f":satellite: [magenta]Checking Account on subnet[/magenta] [blue]{netuid}[/blue][magenta] ...[/magenta]" ) - # We could do this as_completed because we don't actually need old_balance and recycle + # We could do this as_completed because we don't need old_balance and recycle # if neuron is null, but the complexity isn't worth it considering the small performance # gains we'd hypothetically receive in this situation neuron, old_balance, recycle_amount = await asyncio.gather( @@ -126,6 +135,7 @@ async def burned_register_extrinsic( wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) if not success: @@ -162,6 +172,7 @@ async def _do_pow_register( pow_result: "POWSolution", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, + period: Optional[int] = None, ) -> tuple[bool, Optional[str]]: """Sends a (POW) register extrinsic to the chain. @@ -172,6 +183,9 @@ async def _do_pow_register( pow_result (POWSolution): The PoW result to register. wait_for_inclusion (bool): If ``True``, waits for the extrinsic to be included in a block. Default to `False`. wait_for_finalization (bool): If ``True``, waits for the extrinsic to be finalized. Default to `True`. + 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. Returns: success (bool): ``True`` if the extrinsic was included in a block. @@ -196,6 +210,7 @@ async def _do_pow_register( wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) @@ -213,6 +228,7 @@ async def register_extrinsic( num_processes: Optional[int] = None, update_interval: Optional[int] = None, log_verbose: bool = False, + period: Optional[int] = None, ) -> bool: """Registers the wallet to the chain. @@ -233,6 +249,9 @@ async def register_extrinsic( num_processes: The number of processes to use to register. update_interval: The number of nonces to solve between updates. log_verbose: If `True`, the registration process will log more information. + 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. Returns: `True` if extrinsic was finalized or included in the block. If we did not wait for finalization/inclusion, the @@ -321,7 +340,7 @@ async def register_extrinsic( # pow successful, proceed to submit pow to chain for registration else: logging.info(":satellite: [magenta]Submitting POW...[/magenta]") - # check if pow result is still valid + # check if a pow result is still valid while not await pow_result.is_stale_async(subtensor=subtensor): result: tuple[bool, Optional[str]] = await _do_pow_register( subtensor=subtensor, @@ -330,6 +349,7 @@ async def register_extrinsic( pow_result=pow_result, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) success, err_msg = result @@ -386,6 +406,7 @@ async def register_subnet_extrinsic( wallet: "Wallet", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, + period: Optional[int] = None, ) -> bool: """ Registers a new subnetwork on the Bittensor blockchain asynchronously. @@ -395,6 +416,9 @@ async def register_subnet_extrinsic( wallet (Wallet): The wallet to be used for subnet registration. wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning true. wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning true. + 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. Returns: bool: True if the subnet registration was successful, False otherwise. @@ -417,29 +441,25 @@ async def register_subnet_extrinsic( }, ) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - - response = await subtensor.substrate.submit_extrinsic( - extrinsic, + success, message = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) if not wait_for_finalization and not wait_for_inclusion: return True - if not await response.is_success: - logging.error( - f"Failed to register subnet: {format_error_message(await response.error_message)}" + if success: + logging.success( + ":white_heavy_check_mark: [green]Successfully registered subnet[/green]" ) - return False + return True - logging.success( - ":white_heavy_check_mark: [green]Successfully registered subnet[/green]" - ) - return True + logging.error(f"Failed to register subnet: {message}") + return False async def set_subnet_identity_extrinsic( @@ -455,6 +475,7 @@ async def set_subnet_identity_extrinsic( additional: str, wait_for_inclusion: bool = False, wait_for_finalization: bool = True, + period: Optional[int] = None, ) -> tuple[bool, str]: """ Set the identity information for a given subnet. @@ -472,6 +493,9 @@ async def set_subnet_identity_extrinsic( additional (str): Any additional metadata or information related to the subnet. wait_for_inclusion (bool): Whether to wait for the extrinsic inclusion in a block (default: False). wait_for_finalization (bool): Whether to wait for the extrinsic finalization in a block (default: True). + 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. Returns: tuple[bool, str]: A tuple where the first element indicates success or failure (True/False), and the second @@ -498,15 +522,16 @@ async def set_subnet_identity_extrinsic( }, ) - success, error_message = await subtensor.sign_and_send_extrinsic( + success, message = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) if not wait_for_finalization and not wait_for_inclusion: - return True, f"Identities for subnet {netuid} are sent to the chain." + return True, message if success: logging.success( @@ -515,6 +540,6 @@ async def set_subnet_identity_extrinsic( return True, f"Identities for subnet {netuid} are set." logging.error( - f":cross_mark: Failed to set identity for subnet [blue]{netuid}[/blue]: {error_message}" + f":cross_mark: Failed to set identity for subnet [blue]{netuid}[/blue]: {message}" ) - return False, f"Failed to set identity for subnet {netuid}: {error_message}" + return False, f"Failed to set identity for subnet {netuid}: {message}" diff --git a/bittensor/core/extrinsics/asyncex/root.py b/bittensor/core/extrinsics/asyncex/root.py index 6c30f631eb..79c5417e4f 100644 --- a/bittensor/core/extrinsics/asyncex/root.py +++ b/bittensor/core/extrinsics/asyncex/root.py @@ -1,7 +1,6 @@ import asyncio -from typing import Union, TYPE_CHECKING +from typing import Optional, Union, TYPE_CHECKING -from bittensor_wallet import Wallet import numpy as np from numpy.typing import NDArray @@ -12,9 +11,11 @@ from bittensor.utils.weight_utils import ( normalize_max_weight, convert_weights_and_uids_for_emit, + convert_uids_and_weights, ) if TYPE_CHECKING: + from bittensor_wallet import Wallet from bittensor.core.async_subtensor import AsyncSubtensor @@ -47,8 +48,9 @@ async def root_register_extrinsic( wallet: "Wallet", wait_for_inclusion: bool = True, wait_for_finalization: bool = True, + period: Optional[int] = None, ) -> bool: - """Registers the wallet to root network. + """Registers the wallet to the root network. Arguments: subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The AsyncSubtensor object @@ -57,6 +59,9 @@ async def root_register_extrinsic( `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 `True`, or returns `False` if the extrinsic fails to be finalized within the timeout. + 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. Returns: `True` if extrinsic was finalized or included in the block. If we did not wait for finalization/inclusion, @@ -112,15 +117,16 @@ async def root_register_extrinsic( call_function="root_register", call_params={"hotkey": wallet.hotkey.ss58_address}, ) - success, err_msg = await subtensor.sign_and_send_extrinsic( - call, + success, message = await subtensor.sign_and_send_extrinsic( + call=call, wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) if not success: - logging.error(f":cross_mark: [red]Failed error:[/red] {err_msg}") + logging.error(f":cross_mark: [red]Failed error:[/red] {message}") await asyncio.sleep(0.5) return False @@ -151,7 +157,7 @@ async def _do_set_root_weights( version_key: int = 0, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, - period: int = 5, + period: Optional[int] = 8, ) -> tuple[bool, str]: """ Sets the root weights on the Subnet for the given wallet hotkey account. @@ -171,7 +177,9 @@ async def _do_set_root_weights( False. wait_for_finalization (bool, optional): If True, waits for the extrinsic to be finalized on the chain. Defaults to False. - period (int, optional): The period in seconds to wait for extrinsic inclusion or finalization. Defaults to 5. + 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. Returns: tuple: Returns a tuple containing a boolean indicating success and a message describing the result of the @@ -189,30 +197,23 @@ async def _do_set_root_weights( }, ) - next_nonce = await subtensor.substrate.get_account_next_index( - wallet.hotkey.ss58_address - ) - - # Period dictates how long the extrinsic will stay as part of waiting pool - extrinsic = await subtensor.substrate.create_signed_extrinsic( + success, message = await subtensor.sign_and_send_extrinsic( call=call, - keypair=wallet.coldkey, - era={"period": period}, - nonce=next_nonce, - ) - response = await subtensor.substrate.submit_extrinsic( - extrinsic=extrinsic, + wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + use_nonce=True, + period=period, ) + # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: - return True, "Not waiting for finalization or inclusion." + return True, message - if await response.is_success: + if success: return True, "Successfully set weights." - return False, format_error_message(await response.error_message) + return False, message async def set_root_weights_extrinsic( @@ -223,20 +224,24 @@ async def set_root_weights_extrinsic( version_key: int = 0, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, + period: Optional[int] = None, ) -> bool: - """Sets the given weights and values on chain for wallet hotkey account. + """Sets the given weights and values on a chain for a wallet hotkey account. Arguments: subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The AsyncSubtensor object wallet (bittensor_wallet.Wallet): Bittensor wallet object. netuids (Union[NDArray[np.int64], list[int]]): The `netuid` of the subnet to set weights for. - weights (Union[NDArray[np.float32], list[float]]): Weights to set. These must be `float` s and must correspond + weights (Union[NDArray[np.float32], list[Float]]): Weights to set. These must be `Float`s and must correspond to the passed `netuid` s. version_key (int): The version key of the validator. 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 ` True`, or returns `False` if the extrinsic fails to be finalized within the timeout. + 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. Returns: `True` if extrinsic was finalized or included in the block. If we did not wait for finalization/inclusion, the @@ -254,13 +259,10 @@ async def set_root_weights_extrinsic( logging.error(unlock.message) return False - # First convert types. - if isinstance(netuids, list): - netuids = np.array(netuids, dtype=np.int64) - if isinstance(weights, list): - weights = np.array(weights, dtype=np.float32) + # Convert types. + netuids, weights = convert_uids_and_weights(netuids, weights) - logging.debug("Fetching weight limits") + logging.debug("[magenta]Fetching weight limits ...[/magenta]") min_allowed_weights, max_weight_limit = await _get_limits(subtensor) # Get non zero values. @@ -274,7 +276,7 @@ async def set_root_weights_extrinsic( ) # Normalize the weights to max value. - logging.info("Normalizing weights") + logging.info("[magenta]Normalizing weights ...[/magenta]") formatted_weights = normalize_max_weight(x=weights, limit=max_weight_limit) logging.info( f"Raw weights -> Normalized weights: [blue]{weights}[/blue] -> [green]{formatted_weights}[/green]" @@ -292,11 +294,9 @@ async def set_root_weights_extrinsic( version_key=version_key, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) - if not wait_for_finalization and not wait_for_inclusion: - return True - if success is True: logging.info(":white_heavy_check_mark: [green]Finalized[/green]") return True diff --git a/bittensor/core/extrinsics/asyncex/serving.py b/bittensor/core/extrinsics/asyncex/serving.py index 558b58cae5..c37c612191 100644 --- a/bittensor/core/extrinsics/asyncex/serving.py +++ b/bittensor/core/extrinsics/asyncex/serving.py @@ -3,14 +3,13 @@ from bittensor.core.errors import MetadataError from bittensor.core.settings import version_as_int +from bittensor.core.types import AxonServeCallParams from bittensor.utils import ( - format_error_message, networking as net, unlock_key, Certificate, ) from bittensor.utils.btlogging import logging -from bittensor.core.types import AxonServeCallParams if TYPE_CHECKING: from bittensor.core.axon import Axon @@ -24,7 +23,8 @@ async def do_serve_axon( call_params: "AxonServeCallParams", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, -) -> tuple[bool, Optional[dict]]: + period: Optional[int] = None, +) -> tuple[bool, str]: """ Internal method to submit a serve axon transaction to the Bittensor blockchain. This method creates and submits a transaction, enabling a neuron's ``Axon`` to serve requests on the network. @@ -35,9 +35,12 @@ async def do_serve_axon( call_params (bittensor.core.types.AxonServeCallParams): Parameters required for the serve axon call. wait_for_inclusion (bool): Waits for the transaction to be included in a block. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + 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. Returns: - tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. + tuple[bool, str]: A tuple containing a success flag and an optional error message. This function is crucial for initializing and announcing a neuron's ``Axon`` service on the network, enhancing the decentralized computation capabilities of Bittensor. @@ -53,21 +56,14 @@ async def do_serve_axon( call_function=call_function, call_params=call_params.dict(), ) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.hotkey - ) - response = await subtensor.substrate.submit_extrinsic( - extrinsic=extrinsic, + success, message = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) - if wait_for_inclusion or wait_for_finalization: - if await response.is_success: - return True, None - - return False, await response.error_message - - return True, None + return success, message async def serve_extrinsic( @@ -82,6 +78,7 @@ async def serve_extrinsic( wait_for_inclusion: bool = False, wait_for_finalization=True, certificate: Optional[Certificate] = None, + period: Optional[int] = None, ) -> bool: """Subscribes a Bittensor endpoint to the subtensor chain. @@ -100,6 +97,9 @@ async def serve_extrinsic( ``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout. certificate (bittensor.utils.Certificate): Certificate to use for TLS. If ``None``, no TLS will be used. Defaults to ``None``. + 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. Returns: success (bool): Flag is ``True`` if extrinsic was finalized or included in the block. If we did not wait for @@ -132,33 +132,33 @@ async def serve_extrinsic( neuron_up_to_date = not neuron.is_null and params == neuron if neuron_up_to_date: logging.debug( - f"Axon already served on: AxonInfo({wallet.hotkey.ss58_address},{ip}:{port}) " + f"Axon already served on: [blue]AxonInfo({wallet.hotkey.ss58_address}, {ip}:{port})[/blue]" ) return True logging.debug( - f"Serving axon with: AxonInfo({wallet.hotkey.ss58_address},{ip}:{port}) -> {subtensor.network}:{netuid}" + f"Serving axon with: [blue]AxonInfo({wallet.hotkey.ss58_address}, {ip}:{port})[/blue] -> " + f"[green]{subtensor.network}:{netuid}[/green]" ) - success, error_message = await do_serve_axon( + success, message = do_serve_axon( subtensor=subtensor, wallet=wallet, call_params=params, wait_for_finalization=wait_for_finalization, wait_for_inclusion=wait_for_inclusion, + period=period, ) - if wait_for_inclusion or wait_for_finalization: - if success is True: - logging.debug( - f"Axon served with: AxonInfo({wallet.hotkey.ss58_address},{ip}:{port}) on {subtensor.network}:{netuid} " - ) - return True - else: - logging.error(f"Failed: {format_error_message(error_message)}") - return False - else: + if success: + logging.debug( + f"Axon served with: [blue]AxonInfo({wallet.hotkey.ss58_address}, {ip}:{port})[/blue] on " + f"[green]{subtensor.network}:{netuid}[/green]" + ) return True + logging.error(f"Failed: {message}") + return False + async def serve_axon_extrinsic( subtensor: "AsyncSubtensor", @@ -167,6 +167,7 @@ async def serve_axon_extrinsic( wait_for_inclusion: bool = False, wait_for_finalization: bool = True, certificate: Optional[Certificate] = None, + period: Optional[int] = None, ) -> bool: """Serves the axon to the network. @@ -180,6 +181,9 @@ async def serve_axon_extrinsic( ``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout. certificate (bittensor.utils.Certificate): Certificate to use for TLS. If ``None``, no TLS will be used. Defaults to ``None``. + 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. Returns: success (bool): Flag is ``True`` if extrinsic was finalized or included in the block. If we did not wait for @@ -217,6 +221,7 @@ async def serve_axon_extrinsic( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, certificate=certificate, + period=period, ) return serve_success @@ -229,6 +234,7 @@ async def publish_metadata( data: Union[bytes, dict], wait_for_inclusion: bool = False, wait_for_finalization: bool = True, + period: Optional[int] = None, ) -> bool: """ Publishes metadata on the Bittensor network using the specified wallet and network identifier. @@ -246,12 +252,15 @@ async def publish_metadata( block before returning. Defaults to ``False``. wait_for_finalization (bool, optional): If ``True``, the function will wait for the extrinsic to be finalized on the chain before returning. Defaults to ``True``. + 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. Returns: bool: ``True`` if the metadata was successfully published (and finalized if specified). ``False`` otherwise. Raises: - MetadataError: If there is an error in submitting the extrinsic or if the response from the blockchain indicates + MetadataError: If there is an error in submitting the extrinsic, or if the response from the blockchain indicates failure. """ @@ -269,21 +278,17 @@ async def publish_metadata( }, ) - extrinsic = await substrate.create_signed_extrinsic( - call=call, keypair=wallet.hotkey - ) - response = await substrate.submit_extrinsic( - extrinsic=extrinsic, + success, message = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - if await response.is_success: + if success: return True - raise MetadataError(format_error_message(await response.error_message)) + raise MetadataError(message) async def get_metadata( diff --git a/bittensor/core/extrinsics/asyncex/staking.py b/bittensor/core/extrinsics/asyncex/staking.py index ddaa1bd240..41076f0178 100644 --- a/bittensor/core/extrinsics/asyncex/staking.py +++ b/bittensor/core/extrinsics/asyncex/staking.py @@ -1,11 +1,12 @@ import asyncio from typing import Optional, Sequence, TYPE_CHECKING -from bittensor.core.errors import StakeError, NotRegisteredError -from bittensor.utils import unlock_key +from async_substrate_interface.errors import SubstrateRequestException + +from bittensor.core.extrinsics.utils import get_old_stakes +from bittensor.utils import unlock_key, format_error_message from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging -from bittensor.core.extrinsics.utils import get_old_stakes if TYPE_CHECKING: from bittensor_wallet import Wallet @@ -24,6 +25,7 @@ async def add_stake_extrinsic( safe_staking: bool = False, allow_partial_stake: bool = False, rate_tolerance: float = 0.005, + period: Optional[int] = None, ) -> bool: """ Adds the specified amount of stake to passed hotkey `uid`. @@ -42,10 +44,16 @@ async def add_stake_extrinsic( safe_staking: If set, uses safe staking logic allow_partial_stake: If set, allows partial stake rate_tolerance: The rate tolerance for safe staking + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. Returns: success: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for finalization/inclusion, the response is `True`. + + Raises: + SubstrateRequestException: Raised if the extrinsic fails to be included in the block within the timeout. """ # Decrypt keys, @@ -147,13 +155,14 @@ async def add_stake_extrinsic( call_params=call_params, ) staking_response, err_msg = await subtensor.sign_and_send_extrinsic( - call, - wallet, - wait_for_inclusion, - wait_for_finalization, + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, nonce_key="coldkeypub", sign_with="coldkey", use_nonce=True, + period=period, ) if staking_response is True: # If we successfully staked. # We only wait here if we expect finalization. @@ -163,8 +172,8 @@ async def add_stake_extrinsic( logging.success(":white_heavy_check_mark: [green]Finalized[/green]") logging.info( - f":satellite: [magenta]Checking Balance on:[/magenta] [blue]{subtensor.network}[/blue] " - "[magenta]...[/magenta]" + f":satellite: [magenta]Checking Balance on:[/magenta] " + f"[blue]{subtensor.network}[/blue] [magenta]...[/magenta]" ) new_block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( @@ -195,16 +204,11 @@ async def add_stake_extrinsic( logging.error(f":cross_mark: [red]Failed: {err_msg}.[/red]") return False - except NotRegisteredError: + except SubstrateRequestException as error: logging.error( - ":cross_mark: [red]Hotkey: {} is not registered.[/red]".format( - wallet.hotkey_str - ) + f":cross_mark: [red]Add Stake Error: {format_error_message(error)}[/red]" ) return False - except StakeError as e: - logging.error(f":cross_mark: [red]Stake Error: {e}[/red]") - return False async def add_stake_multiple_extrinsic( @@ -216,8 +220,9 @@ async def add_stake_multiple_extrinsic( amounts: Optional[list[Balance]] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + period: Optional[int] = None, ) -> bool: - """Adds stake to each ``hotkey_ss58`` in the list, using each amount, from a common coldkey. + """Adds a stake to each ``hotkey_ss58`` in the list, using each amount, from a common coldkey. Arguments: subtensor: The initialized SubtensorInterface object. @@ -230,6 +235,9 @@ async def add_stake_multiple_extrinsic( if the extrinsic fails to enter the block within the timeout. wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, or returns `False` if the extrinsic fails to be finalized within the timeout. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. Returns: success: `True` if extrinsic was finalized or included in the block. `True` if any wallet was staked. If we did @@ -341,17 +349,18 @@ async def add_stake_multiple_extrinsic( "netuid": netuid, }, ) - staking_response, err_msg = await subtensor.sign_and_send_extrinsic( - call, - wallet, - wait_for_inclusion, - wait_for_finalization, + success, message = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, nonce_key="coldkeypub", sign_with="coldkey", use_nonce=True, + period=period, ) - if staking_response is True: # If we successfully staked. + if success is True: # If we successfully staked. # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: @@ -391,17 +400,14 @@ async def add_stake_multiple_extrinsic( break else: - logging.error(f":cross_mark: [red]Failed: {err_msg}.[/red]") + logging.error(f":cross_mark: [red]Failed: {message}.[/red]") continue - except NotRegisteredError: + except SubstrateRequestException as error: logging.error( - f":cross_mark: [red]Hotkey: {hotkey_ss58} is not registered.[/red]" + f":cross_mark: [red]Add Stake Multiple error: {format_error_message(error)}[/red]" ) continue - except StakeError as e: - logging.error(f":cross_mark: [red]Stake Error: {e}[/red]") - continue if successful_stakes != 0: logging.info( diff --git a/bittensor/core/extrinsics/asyncex/start_call.py b/bittensor/core/extrinsics/asyncex/start_call.py index bc0089ddb9..63f6fbc3c1 100644 --- a/bittensor/core/extrinsics/asyncex/start_call.py +++ b/bittensor/core/extrinsics/asyncex/start_call.py @@ -1,6 +1,6 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional -from bittensor.utils import unlock_key, format_error_message +from bittensor.utils import unlock_key from bittensor.utils.btlogging import logging if TYPE_CHECKING: @@ -14,6 +14,7 @@ async def start_call_extrinsic( netuid: int, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + period: Optional[int] = None, ) -> tuple[bool, str]: """ Submits a start_call extrinsic to the blockchain, to trigger the start call process for a subnet (used to start a @@ -25,6 +26,9 @@ async def start_call_extrinsic( netuid (int): The UID of the target subnet for which the call is being initiated. wait_for_inclusion (bool, optional): Whether to wait for the extrinsic to be included in a block. Defaults to True. wait_for_finalization (bool, optional): Whether to wait for finalization of the extrinsic. Defaults to False. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. Returns: Tuple[bool, str]: @@ -41,21 +45,19 @@ async def start_call_extrinsic( call_function="start_call", call_params={"netuid": netuid}, ) - signed_ext = await substrate.create_signed_extrinsic( - call=start_call, - keypair=wallet.coldkey, - ) - response = await substrate.submit_extrinsic( - extrinsic=signed_ext, + success, message = await subtensor.sign_and_send_extrinsic( + call=start_call, + wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) if not wait_for_finalization and not wait_for_inclusion: - return True, "Not waiting for finalization or inclusion." + return True, message - if await response.is_success: + if success: return True, "Success with `start_call` response." - return False, format_error_message(await response.error_message) + return True, message diff --git a/bittensor/core/extrinsics/asyncex/take.py b/bittensor/core/extrinsics/asyncex/take.py index 6a51239bc0..543d4e72da 100644 --- a/bittensor/core/extrinsics/asyncex/take.py +++ b/bittensor/core/extrinsics/asyncex/take.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from bittensor_wallet.bittensor_wallet import Wallet @@ -16,7 +16,26 @@ async def increase_take_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = True, raise_error: bool = False, + period: Optional[int] = None, ) -> tuple[bool, str]: + """Sets the delegate 'take' percentage for a neuron identified by its hotkey. + + Args: + subtensor (Subtensor): Blockchain connection. + wallet (Wallet): The wallet to sign the extrinsic. + hotkey_ss58 (str): SS58 address of the hotkey to set take for. + take (int): The percentage of rewards that the delegate claims from nominators. + wait_for_inclusion (bool, optional): Wait for inclusion before returning. Defaults to True. + wait_for_finalization (bool, optional): Wait for finalization before returning. Defaults to True. + raise_error (bool, optional): Raise error on failure. Defaults to False. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. + + Returns: + tuple[bool, str]: Success flag and status message. + """ + unlock = unlock_key(wallet, raise_error=raise_error) if not unlock.success: @@ -32,10 +51,11 @@ async def increase_take_extrinsic( ) return await subtensor.sign_and_send_extrinsic( - call, - wallet, + call=call, + wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, raise_error=raise_error, ) @@ -48,7 +68,25 @@ async def decrease_take_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = True, raise_error: bool = False, + period: Optional[int] = None, ) -> tuple[bool, str]: + """Sets the delegate 'take' percentage for a neuron identified by its hotkey. + + Args: + subtensor (Subtensor): Blockchain connection. + wallet (Wallet): The wallet to sign the extrinsic. + hotkey_ss58 (str): SS58 address of the hotkey to set take for. + take (int): The percentage of rewards that the delegate claims from nominators. + wait_for_inclusion (bool, optional): Wait for inclusion before returning. Defaults to True. + wait_for_finalization (bool, optional): Wait for finalization before returning. Defaults to True. + raise_error (bool, optional): Raise error on failure. Defaults to False. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. + + Returns: + tuple[bool, str]: Success flag and status message. + """ unlock = unlock_key(wallet, raise_error=raise_error) if not unlock.success: @@ -64,9 +102,10 @@ async def decrease_take_extrinsic( ) return await subtensor.sign_and_send_extrinsic( - call, - wallet, + call=call, + wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, raise_error=raise_error, ) diff --git a/bittensor/core/extrinsics/asyncex/transfer.py b/bittensor/core/extrinsics/asyncex/transfer.py index 1347c44260..a1c781310c 100644 --- a/bittensor/core/extrinsics/asyncex/transfer.py +++ b/bittensor/core/extrinsics/asyncex/transfer.py @@ -1,9 +1,8 @@ import asyncio -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from bittensor.core.settings import NETWORK_EXPLORER_MAP from bittensor.utils import ( - format_error_message, get_explorer_url_for_network, is_valid_bittensor_address_or_public_key, unlock_key, @@ -23,6 +22,7 @@ async def _do_transfer( amount: "Balance", wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + period: Optional[int] = None, ) -> tuple[bool, str, str]: """ Makes transfer from wallet to destination public key address. @@ -34,8 +34,11 @@ async def _do_transfer( amount (bittensor.utils.balance.Balance): Amount to stake as Bittensor balance. 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 + wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning `True`, or returns `False` if the extrinsic fails to be finalized within the timeout. + 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. Returns: success, block hash, formatted error message @@ -45,24 +48,25 @@ async def _do_transfer( call_function="transfer_allow_death", call_params={"dest": destination, "value": amount.rao}, ) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = await subtensor.substrate.submit_extrinsic( - extrinsic=extrinsic, + + success, message = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) + # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: - return True, "", "Success, extrinsic submitted without waiting." + return True, "", message # Otherwise continue with finalization. - if await response.is_success: - block_hash_ = response.block_hash + if success: + block_hash_ = await subtensor.get_block_hash() return True, block_hash_, "Success with response." - return False, "", format_error_message(await response.error_message) + return False, "", message async def transfer_extrinsic( @@ -74,6 +78,7 @@ async def transfer_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, keep_alive: bool = True, + period: Optional[int] = None, ) -> bool: """Transfers funds from this wallet to the destination public key address. @@ -85,9 +90,12 @@ async def transfer_extrinsic( 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. - wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning + wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning `True`, or returns `False` if the extrinsic fails to be finalized within the timeout. keep_alive (bool): If set, keeps the account alive by keeping the balance above the existential deposit. + 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. Returns: success (bool): Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for @@ -100,6 +108,7 @@ async def transfer_extrinsic( f":cross_mark: [red]Invalid destination SS58 address[/red]: {destination}" ) return False + logging.info(f"Initiating transfer on network: {subtensor.network}") # Unlock wallet coldkey. if not (unlock := unlock_key(wallet)).success: @@ -148,6 +157,7 @@ async def transfer_extrinsic( amount=amount, wait_for_finalization=wait_for_finalization, wait_for_inclusion=wait_for_inclusion, + period=period, ) if success: @@ -173,6 +183,6 @@ async def transfer_extrinsic( f"Balance: [blue]{account_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" ) return True - else: - logging.error(f":cross_mark: [red]Failed[/red]: {err_msg}") - return False + + logging.error(f":cross_mark: [red]Failed[/red]: {err_msg}") + return False diff --git a/bittensor/core/extrinsics/asyncex/unstaking.py b/bittensor/core/extrinsics/asyncex/unstaking.py index 54fb43c79d..8125b603e9 100644 --- a/bittensor/core/extrinsics/asyncex/unstaking.py +++ b/bittensor/core/extrinsics/asyncex/unstaking.py @@ -1,11 +1,12 @@ import asyncio from typing import Optional, TYPE_CHECKING -from bittensor.core.errors import StakeError, NotRegisteredError -from bittensor.utils import unlock_key +from async_substrate_interface.errors import SubstrateRequestException + +from bittensor.core.extrinsics.utils import get_old_stakes +from bittensor.utils import unlock_key, format_error_message from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging -from bittensor.core.extrinsics.utils import get_old_stakes if TYPE_CHECKING: from bittensor_wallet import Wallet @@ -23,6 +24,7 @@ async def unstake_extrinsic( safe_staking: bool = False, allow_partial_stake: bool = False, rate_tolerance: float = 0.005, + period: Optional[int] = None, ) -> bool: """Removes stake into the wallet coldkey from the specified hotkey ``uid``. @@ -40,6 +42,9 @@ async def unstake_extrinsic( safe_staking: If true, enables price safety checks allow_partial_stake: If true, allows partial unstaking if price tolerance exceeded rate_tolerance: Maximum allowed price decrease percentage (0.005 = 0.5%) + 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. Returns: success (bool): Flag is ``True`` if extrinsic was finalized or included in the block. If we did not wait for @@ -133,17 +138,18 @@ async def unstake_extrinsic( call_function=call_function, call_params=call_params, ) - staking_response, err_msg = await subtensor.sign_and_send_extrinsic( - call, - wallet, - wait_for_inclusion, - wait_for_finalization, + success, message = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, nonce_key="coldkeypub", sign_with="coldkey", use_nonce=True, + period=period, ) - if staking_response is True: # If we successfully unstaked. + if success is True: # If we successfully unstaked. # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: return True @@ -174,22 +180,19 @@ async def unstake_extrinsic( ) return True else: - if safe_staking and "Custom error: 8" in err_msg: + if safe_staking and "Custom error: 8" in message: logging.error( ":cross_mark: [red]Failed[/red]: Price exceeded tolerance limit. Either increase price tolerance or enable partial staking." ) else: - logging.error(f":cross_mark: [red]Failed: {err_msg}.[/red]") + logging.error(f":cross_mark: [red]Failed: {message}.[/red]") return False - except NotRegisteredError: + except SubstrateRequestException as error: logging.error( - f":cross_mark: [red]Hotkey: {wallet.hotkey_str} is not registered.[/red]" + f":cross_mark: [red]Unstake filed with error: {format_error_message(error)}[/red]" ) return False - except StakeError as e: - logging.error(f":cross_mark: [red]Stake Error: {e}[/red]") - return False async def unstake_multiple_extrinsic( @@ -200,6 +203,7 @@ async def unstake_multiple_extrinsic( amounts: Optional[list[Balance]] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + period: Optional[int] = None, ) -> bool: """Removes stake from each ``hotkey_ss58`` in the list, using each amount, to a common coldkey. @@ -213,6 +217,9 @@ async def unstake_multiple_extrinsic( 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 ``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout. + 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. Returns: success (bool): Flag is ``True`` if extrinsic was finalized or included in the block. Flag is ``True`` if any @@ -310,13 +317,14 @@ async def unstake_multiple_extrinsic( ) staking_response, err_msg = await subtensor.sign_and_send_extrinsic( - call, - wallet, - wait_for_inclusion, - wait_for_finalization, + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, nonce_key="coldkeypub", sign_with="coldkey", use_nonce=True, + period=period, ) if staking_response is True: # If we successfully unstaked. @@ -347,14 +355,11 @@ async def unstake_multiple_extrinsic( logging.error(f":cross_mark: [red]Failed: {err_msg}.[/red]") continue - except NotRegisteredError: + except SubstrateRequestException as error: logging.error( - f":cross_mark: [red]Hotkey[/red] [blue]{hotkey_ss58}[/blue] [red]is not registered.[/red]" + f":cross_mark: [red]Multiple unstake filed with error: {format_error_message(error)}[/red]" ) - continue - except StakeError as e: - logging.error(f":cross_mark: [red]Stake Error: {e}[/red]") - continue + return False if successful_unstakes != 0: logging.info( diff --git a/bittensor/core/extrinsics/asyncex/weights.py b/bittensor/core/extrinsics/asyncex/weights.py index 6e07b90adb..cf993b37eb 100644 --- a/bittensor/core/extrinsics/asyncex/weights.py +++ b/bittensor/core/extrinsics/asyncex/weights.py @@ -5,10 +5,9 @@ import numpy as np from numpy.typing import NDArray -import bittensor.utils.weight_utils as weight_utils from bittensor.core.settings import version_as_int -from bittensor.utils import format_error_message from bittensor.utils.btlogging import logging +from bittensor.utils.weight_utils import convert_and_normalize_weights_and_uids if TYPE_CHECKING: from bittensor_wallet import Wallet @@ -23,22 +22,27 @@ async def _do_commit_weights( commit_hash: str, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, -) -> tuple[bool, Optional[str]]: + period: Optional[int] = None, +) -> tuple[bool, str]: """ Internal method to send a transaction to the Bittensor blockchain, committing the hash of a neuron's weights. This method constructs and submits the transaction, handling retries and blockchain communication. Args: - subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance used for blockchain - interaction. + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance used for blockchain interaction. wallet (bittensor_wallet.Wallet): The wallet associated with the neuron committing the weights. netuid (int): The unique identifier of the subnet. commit_hash (str): The hash of the neuron's weights to be committed. wait_for_inclusion (bool): Waits for the transaction to be included in a block. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + 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. Returns: - tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. + tuple[bool, str]: + `True` if the weight commitment is successful, `False` otherwise. + `msg` is a string value describing the success or potential error. This method ensures that the weight commitment is securely recorded on the Bittensor blockchain, providing a verifiable record of the neuron's weight distribution at a specific point in time. @@ -52,11 +56,12 @@ async def _do_commit_weights( }, ) return await subtensor.sign_and_send_extrinsic( - call, - wallet, - wait_for_inclusion, - wait_for_finalization, + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, use_nonce=True, + period=period, nonce_key="hotkey", sign_with="hotkey", ) @@ -69,6 +74,7 @@ async def commit_weights_extrinsic( commit_hash: str, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, + period: Optional[int] = None, ) -> tuple[bool, str]: """ Commits a hash of the neuron's weights to the Bittensor blockchain using the provided wallet. @@ -82,15 +88,18 @@ async def commit_weights_extrinsic( commit_hash (str): The hash of the neuron's weights to be committed. wait_for_inclusion (bool): Waits for the transaction to be included in a block. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + 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. Returns: - tuple[bool, str]: ``True`` if the weight commitment is successful, False otherwise. And `msg`, a string - value describing the success or potential error. + tuple[bool, str]: + `True` if the weight commitment is successful, `False` otherwise. + `msg` is a string value describing the success or potential error. This function provides a user-friendly interface for committing weights to the Bittensor blockchain, ensuring proper error handling and user interaction when required. """ - success, error_message = await _do_commit_weights( subtensor=subtensor, wallet=wallet, @@ -98,6 +107,7 @@ async def commit_weights_extrinsic( commit_hash=commit_hash, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) if success: @@ -119,7 +129,8 @@ async def _do_reveal_weights( version_key: int, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, -) -> tuple[bool, Optional[dict]]: + period: Optional[int] = None, +) -> tuple[bool, str]: """ Internal method to send a transaction to the Bittensor blockchain, revealing the weights for a specific subnet. This method constructs and submits the transaction, handling retries and blockchain communication. @@ -135,14 +146,18 @@ async def _do_reveal_weights( version_key (int): Version key for compatibility with the network. wait_for_inclusion (bool): Waits for the transaction to be included in a block. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + 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. Returns: - tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. + tuple[bool, str]: + `True` if the weight commitment is successful, `False` otherwise. + `msg` is a string value describing the success or potential error. This method ensures that the weight revelation is securely recorded on the Bittensor blockchain, providing transparency and accountability for the neuron's weight distribution. """ - call = await subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="reveal_weights", @@ -155,11 +170,12 @@ async def _do_reveal_weights( }, ) return await subtensor.sign_and_send_extrinsic( - call, - wallet, - wait_for_inclusion, - wait_for_finalization, + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, sign_with="hotkey", + period=period, nonce_key="hotkey", use_nonce=True, ) @@ -175,14 +191,14 @@ async def reveal_weights_extrinsic( version_key: int, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, + period: Optional[int] = None, ) -> tuple[bool, str]: """ Reveals the weights for a specific subnet on the Bittensor blockchain using the provided wallet. This function is a wrapper around the `_do_reveal_weights` method. Args: - subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance used for blockchain - interaction. + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance used for blockchain interaction. wallet (bittensor_wallet.Wallet): The wallet associated with the neuron revealing the weights. netuid (int): The unique identifier of the subnet. uids (list[int]): List of neuron UIDs for which weights are being revealed. @@ -191,15 +207,18 @@ async def reveal_weights_extrinsic( version_key (int): Version key for compatibility with the network. wait_for_inclusion (bool): Waits for the transaction to be included in a block. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + 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. Returns: - tuple[bool, str]: ``True`` if the weight revelation is successful, False otherwise. And `msg`, a string value - describing the success or potential error. + tuple[bool, str]: + `True` if the weight commitment is successful, `False` otherwise. + `msg` is a string value describing the success or potential error. This function provides a user-friendly interface for revealing weights on the Bittensor blockchain, ensuring proper error handling and user interaction when required. """ - success, error_message = await _do_reveal_weights( subtensor=subtensor, wallet=wallet, @@ -210,6 +229,7 @@ async def reveal_weights_extrinsic( version_key=version_key, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) if success: @@ -217,7 +237,6 @@ async def reveal_weights_extrinsic( logging.info(success_message) return True, success_message - error_message = format_error_message(error_message) logging.error(f"Failed to reveal weights: {error_message}") return False, error_message @@ -231,12 +250,11 @@ async def _do_set_weights( version_key: int = version_as_int, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, - period: int = 5, -) -> tuple[bool, Optional[str]]: # (success, error_message) + period: Optional[int] = None, +) -> tuple[bool, str]: # (success, error_message) """ - Internal method to send a transaction to the Bittensor blockchain, setting weights - for specified neurons. This method constructs and submits the transaction, handling - retries and blockchain communication. + Internal method to send a transaction to the Bittensor blockchain, setting weights for specified neurons. This + method constructs and submits the transaction, handling retries and blockchain communication. Args: subtensor (subtensor.core.async_subtensor.AsyncSubtensor): Async Subtensor instance. @@ -247,15 +265,18 @@ async def _do_set_weights( version_key (int, optional): Version key for compatibility with the network. wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. wait_for_finalization (bool, optional): Waits for the transaction to be finalized on the blockchain. - period (int, optional): The period in seconds to wait for extrinsic inclusion or finalization. Defaults to 5. + 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. Returns: - Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. + tuple[bool, str]: + `True` if the weight commitment is successful, `False` otherwise. + `msg` is a string value describing the success or potential error. This method is vital for the dynamic weighting mechanism in Bittensor, where neurons adjust their trust in other neurons based on observed performance and contributions. """ - call = await subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="set_weights", @@ -266,17 +287,25 @@ async def _do_set_weights( "version_key": version_key, }, ) - return await subtensor.sign_and_send_extrinsic( - call, - wallet, - wait_for_inclusion, - wait_for_finalization, + success, message = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, period=period, use_nonce=True, nonce_key="hotkey", sign_with="hotkey", ) + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + return True, "Not waiting for finalization or inclusion." + + if success: + return success, "Successfully set weights." + return success, message + async def set_weights_extrinsic( subtensor: "AsyncSubtensor", @@ -287,9 +316,9 @@ async def set_weights_extrinsic( version_key: int = 0, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, - period: int = 5, + period: Optional[int] = 8, ) -> tuple[bool, str]: - """Sets the given weights and values on chain for wallet hotkey account. + """Sets the given weights and values on chain for a given wallet hotkey account. Args: subtensor (bittensor.core.async_subtensor.AsyncSubtensor): Bittensor subtensor object. @@ -303,28 +332,24 @@ async def set_weights_extrinsic( 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 ``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout. - period (int, optional): The period in seconds to wait for extrinsic inclusion or finalization. Defaults to 5. + 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. Returns: - 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``. + tuple[bool, str]: + `True` if the weight commitment is successful, `False` otherwise. + `msg` is a string value describing the success or potential error. """ - # First convert types. - if isinstance(uids, list): - uids = np.array(uids, dtype=np.int64) - if isinstance(weights, list): - weights = np.array(weights, dtype=np.float32) - - # Reformat and normalize. - weight_uids, weight_vals = weight_utils.convert_weights_and_uids_for_emit( - uids, weights - ) + weight_uids, weight_vals = convert_and_normalize_weights_and_uids(uids, weights) logging.info( - f":satellite: [magenta]Setting weights on [/magenta][blue]{subtensor.network}[/blue] [magenta]...[/magenta]" + f":satellite: [magenta]Setting weights on [/magenta]" + f"[blue]{subtensor.network}[/blue] " + f"[magenta]...[/magenta]" ) try: - success, error_message = await _do_set_weights( + success, message = await _do_set_weights( subtensor=subtensor, wallet=wallet, netuid=netuid, @@ -337,15 +362,15 @@ async def set_weights_extrinsic( ) if not wait_for_finalization and not wait_for_inclusion: - return True, "Not waiting for finalization or inclusion." + return True, message if success is True: message = "Successfully set weights and Finalized." logging.success(f":white_heavy_check_mark: [green]{message}[/green]") return True, message - logging.error(f"[red]Failed[/red] set weights. Error: {error_message}") - return False, error_message + logging.error(f"[red]Failed[/red] set weights. Error: {message}") + return False, message except Exception as error: logging.error(f":cross_mark: [red]Failed[/red] set weights. Error: {error}") diff --git a/bittensor/core/extrinsics/commit_reveal.py b/bittensor/core/extrinsics/commit_reveal.py index 86795f1624..c531986254 100644 --- a/bittensor/core/extrinsics/commit_reveal.py +++ b/bittensor/core/extrinsics/commit_reveal.py @@ -2,13 +2,13 @@ from typing import Union, TYPE_CHECKING, Optional -from bittensor_commit_reveal import get_encrypted_commit import numpy as np +from bittensor_drand import get_encrypted_commit from numpy.typing import NDArray from bittensor.core.settings import version_as_int from bittensor.utils.btlogging import logging -from bittensor.utils.weight_utils import convert_weights_and_uids_for_emit +from bittensor.utils.weight_utils import convert_and_normalize_weights_and_uids if TYPE_CHECKING: from bittensor_wallet import Wallet @@ -24,23 +24,27 @@ def _do_commit_reveal_v3( reveal_round: int, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, -) -> tuple[bool, Optional[str]]: + period: Optional[int] = None, +) -> tuple[bool, str]: """ - Executes the commit-reveal phase 3 for a given netuid and commit, and optionally waits for extrinsic inclusion or + Executes commit-reveal phase 3 for a given netuid and commit, and optionally waits for extrinsic inclusion or finalization. Arguments: subtensor: An instance of the Subtensor class. wallet: Wallet An instance of the Wallet class containing the user's keypair. netuid: int The network unique identifier. - commit bytes The commit data in bytes format. + commit: bytes The commit data in bytes format. reveal_round: int The round number for the reveal phase. 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 + 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. Returns: - A tuple where the first element is a boolean indicating success or failure, and the second element is an - optional string containing error message if any. + A tuple where the first element is a boolean indicating success or failure, and the second element is a string + containing an error message if any. """ logging.info( f"Committing weights hash [blue]{commit.hex()}[/blue] for subnet #[blue]{netuid}[/blue] with " @@ -57,7 +61,12 @@ def _do_commit_reveal_v3( }, ) return subtensor.sign_and_send_extrinsic( - call, wallet, wait_for_inclusion, wait_for_finalization, sign_with="hotkey" + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + sign_with="hotkey", + period=period, ) @@ -70,10 +79,11 @@ def commit_reveal_v3_extrinsic( version_key: int = version_as_int, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, - block_time: float = 12.0, + block_time: Union[int, float] = 12.0, + period: Optional[int] = None, ) -> tuple[bool, str]: """ - Commits and reveals weights for given subtensor and wallet with provided uids and weights. + Commits and reveals weights for a given subtensor and wallet with provided uids and weights. Arguments: subtensor: The Subtensor instance. @@ -84,21 +94,17 @@ def commit_reveal_v3_extrinsic( version_key: The version key to use for committing and revealing. Default is version_as_int. wait_for_inclusion: Whether to wait for the inclusion of the transaction. Default is False. wait_for_finalization: Whether to wait for the finalization of the transaction. Default is False. - block_time (float): The amount of seconds for block duration. Default is 12.0 seconds. + block_time (float): The number of seconds for block duration. Default is 12.0 seconds. + 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. Returns: tuple[bool, str]: A tuple where the first element is a boolean indicating success or failure, and the second element is a message associated with the result """ try: - # Convert uids and weights - if isinstance(uids, list): - uids = np.array(uids, dtype=np.int64) - if isinstance(weights, list): - weights = np.array(weights, dtype=np.float32) - - # Reformat and normalize. - uids, weights = convert_weights_and_uids_for_emit(uids, weights) + uids, weights = convert_and_normalize_weights_and_uids(uids, weights) current_block = subtensor.get_current_block() subnet_hyperparameters = subtensor.get_subnet_hyperparameters( @@ -127,6 +133,7 @@ def commit_reveal_v3_extrinsic( reveal_round=reveal_round, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) if success is not True: diff --git a/bittensor/core/extrinsics/commit_weights.py b/bittensor/core/extrinsics/commit_weights.py index 06e3295eb5..511d1364ef 100644 --- a/bittensor/core/extrinsics/commit_weights.py +++ b/bittensor/core/extrinsics/commit_weights.py @@ -2,7 +2,6 @@ from typing import TYPE_CHECKING, Optional -from bittensor.utils import format_error_message from bittensor.utils.btlogging import logging if TYPE_CHECKING: @@ -17,7 +16,8 @@ def _do_commit_weights( commit_hash: str, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, -) -> tuple[bool, Optional[str]]: + period: Optional[int] = None, +) -> tuple[bool, str]: """ Internal method to send a transaction to the Bittensor blockchain, committing the hash of a neuron's weights. This method constructs and submits the transaction, handling retries and blockchain communication. @@ -29,9 +29,14 @@ def _do_commit_weights( commit_hash (str): The hash of the neuron's weights to be committed. wait_for_inclusion (bool): Waits for the transaction to be included in a block. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + 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. Returns: - tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. + tuple[bool, str]: + `True` if the weight commitment is successful, `False` otherwise. + `msg` is a string value describing the success or potential error. This method ensures that the weight commitment is securely recorded on the Bittensor blockchain, providing a verifiable record of the neuron's weight distribution at a specific point in time. @@ -45,11 +50,12 @@ def _do_commit_weights( }, ) return subtensor.sign_and_send_extrinsic( - call, - wallet, - wait_for_inclusion, - wait_for_finalization, + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, use_nonce=True, + period=period, sign_with="hotkey", nonce_key="hotkey", ) @@ -62,6 +68,7 @@ def commit_weights_extrinsic( commit_hash: str, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, + period: Optional[int] = None, ) -> tuple[bool, str]: """ Commits a hash of the neuron's weights to the Bittensor blockchain using the provided wallet. @@ -74,10 +81,14 @@ def commit_weights_extrinsic( commit_hash (str): The hash of the neuron's weights to be committed. wait_for_inclusion (bool): Waits for the transaction to be included in a block. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + 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. Returns: - tuple[bool, str]: ``True`` if the weight commitment is successful, False otherwise. And `msg`, a string - value describing the success or potential error. + tuple[bool, str]: + `True` if the weight commitment is successful, `False` otherwise. + `msg` is a string value describing the success or potential error. This function provides a user-friendly interface for committing weights to the Bittensor blockchain, ensuring proper error handling and user interaction when required. @@ -90,6 +101,7 @@ def commit_weights_extrinsic( commit_hash=commit_hash, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) if success: @@ -111,7 +123,8 @@ def _do_reveal_weights( version_key: int, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, -) -> tuple[bool, Optional[dict]]: + period: Optional[int] = None, +) -> tuple[bool, str]: """ Internal method to send a transaction to the Bittensor blockchain, revealing the weights for a specific subnet. This method constructs and submits the transaction, handling retries and blockchain communication. @@ -126,9 +139,14 @@ def _do_reveal_weights( version_key (int): Version key for compatibility with the network. wait_for_inclusion (bool): Waits for the transaction to be included in a block. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + 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. Returns: - tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. + tuple[bool, str]: + `True` if the weight commitment is successful, `False` otherwise. + `msg` is a string value describing the success or potential error. This method ensures that the weight revelation is securely recorded on the Bittensor blockchain, providing transparency and accountability for the neuron's weight distribution. @@ -146,11 +164,12 @@ def _do_reveal_weights( }, ) return subtensor.sign_and_send_extrinsic( - call, - wallet, - wait_for_inclusion, - wait_for_finalization, + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, use_nonce=True, + period=period, sign_with="hotkey", nonce_key="hotkey", ) @@ -166,6 +185,7 @@ def reveal_weights_extrinsic( version_key: int, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, + period: Optional[int] = None, ) -> tuple[bool, str]: """ Reveals the weights for a specific subnet on the Bittensor blockchain using the provided wallet. @@ -181,10 +201,14 @@ def reveal_weights_extrinsic( version_key (int): Version key for compatibility with the network. wait_for_inclusion (bool): Waits for the transaction to be included in a block. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + 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. Returns: - tuple[bool, str]: ``True`` if the weight revelation is successful, False otherwise. And `msg`, a string value - describing the success or potential error. + tuple[bool, str]: + `True` if the weight commitment is successful, `False` otherwise. + `msg` is a string value describing the success or potential error. This function provides a user-friendly interface for revealing weights on the Bittensor blockchain, ensuring proper error handling and user interaction when required. @@ -200,6 +224,7 @@ def reveal_weights_extrinsic( version_key=version_key, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) if success: @@ -207,6 +232,6 @@ def reveal_weights_extrinsic( logging.info(success_message) return True, success_message - error_message = format_error_message(error_message) + error_message = error_message logging.error(f"Failed to reveal weights: {error_message}") return False, error_message diff --git a/bittensor/core/extrinsics/move_stake.py b/bittensor/core/extrinsics/move_stake.py index fdf9d406a8..3e7e49eefd 100644 --- a/bittensor/core/extrinsics/move_stake.py +++ b/bittensor/core/extrinsics/move_stake.py @@ -17,6 +17,7 @@ def _get_stake_in_origin_and_dest( origin_netuid: int, destination_netuid: int, ) -> tuple[Balance, Balance]: + """Gets the current stake balances for both origin and destination addresses in their respective subnets.""" block = subtensor.get_current_block() stake_in_origin = subtensor.get_stake( coldkey_ss58=origin_coldkey_ss58, @@ -43,6 +44,7 @@ def transfer_stake_extrinsic( amount: Optional[Balance] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + period: Optional[int] = None, ) -> bool: """ Transfers stake from one subnet to another while changing the coldkey owner. @@ -57,6 +59,9 @@ def transfer_stake_extrinsic( amount (Union[Balance, float, int]): Amount to transfer. wait_for_inclusion (bool): If true, waits for inclusion before returning. wait_for_finalization (bool): If true, waits for finalization before returning. + 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. Returns: success (bool): True if the transfer was successful. @@ -113,6 +118,7 @@ def transfer_stake_extrinsic( wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) if success: @@ -160,6 +166,7 @@ def swap_stake_extrinsic( safe_staking: bool = False, allow_partial_stake: bool = False, rate_tolerance: float = 0.005, + period: Optional[int] = None, ) -> bool: """ Moves stake between subnets while keeping the same coldkey-hotkey pair ownership. @@ -175,7 +182,10 @@ def swap_stake_extrinsic( wait_for_finalization (bool): If true, waits for finalization before returning. safe_staking (bool): If true, enables price safety checks to protect against price impact. allow_partial_stake (bool): If true, allows partial stake swaps when the full amount would exceed the price tolerance. - rate_tolerance (float): Maximum allowed increase in price ratio (0.005 = 0.5%). + rate_tolerance (float): Maximum allowed increase in a price ratio (0.005 = 0.5%). + 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. Returns: success (bool): True if the swap was successful. @@ -193,7 +203,7 @@ def swap_stake_extrinsic( # Check sufficient stake stake_in_origin, stake_in_destination = _get_stake_in_origin_and_dest( - subtensor, + subtensor=subtensor, origin_hotkey_ss58=hotkey_ss58, destination_hotkey_ss58=hotkey_ss58, origin_netuid=origin_netuid, @@ -255,6 +265,7 @@ def swap_stake_extrinsic( wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) if success: @@ -265,7 +276,7 @@ def swap_stake_extrinsic( # Get updated stakes origin_stake, dest_stake = _get_stake_in_origin_and_dest( - subtensor, + subtensor=subtensor, origin_hotkey_ss58=hotkey_ss58, destination_hotkey_ss58=hotkey_ss58, origin_netuid=origin_netuid, @@ -305,6 +316,7 @@ def move_stake_extrinsic( amount: Optional[Balance] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + period: Optional[int] = None, ) -> bool: """ Moves stake to a different hotkey and/or subnet while keeping the same coldkey owner. @@ -319,6 +331,9 @@ def move_stake_extrinsic( amount (Union[Balance, float]): Amount to move. wait_for_inclusion (bool): If true, waits for inclusion before returning. wait_for_finalization (bool): If true, waits for finalization before returning. + 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. Returns: success (bool): True if the move was successful. @@ -328,7 +343,7 @@ def move_stake_extrinsic( # Check sufficient stake stake_in_origin, stake_in_destination = _get_stake_in_origin_and_dest( - subtensor, + subtensor=subtensor, origin_hotkey_ss58=origin_hotkey, destination_hotkey_ss58=destination_hotkey, origin_netuid=origin_netuid, @@ -364,6 +379,7 @@ def move_stake_extrinsic( wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) if success: @@ -374,7 +390,7 @@ def move_stake_extrinsic( # Get updated stakes origin_stake, dest_stake = _get_stake_in_origin_and_dest( - subtensor, + subtensor=subtensor, origin_hotkey_ss58=origin_hotkey, destination_hotkey_ss58=destination_hotkey, origin_netuid=origin_netuid, diff --git a/bittensor/core/extrinsics/registration.py b/bittensor/core/extrinsics/registration.py index c1feea3a30..9acd2f0f8d 100644 --- a/bittensor/core/extrinsics/registration.py +++ b/bittensor/core/extrinsics/registration.py @@ -25,6 +25,7 @@ def _do_burned_register( wallet: "Wallet", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, + period: Optional[int] = None, ) -> tuple[bool, str]: """ Performs a burned register extrinsic call to the Subtensor chain. @@ -37,6 +38,9 @@ def _do_burned_register( wallet (bittensor_wallet.Wallet): The wallet to be registered. wait_for_inclusion (bool): Whether to wait for the transaction to be included in a block. Default is False. wait_for_finalization (bool): Whether to wait for the transaction to be finalized. Default is True. + 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. Returns: Tuple[bool, Optional[str]]: A tuple containing a boolean indicating success or failure, and an optional error @@ -57,6 +61,7 @@ def _do_burned_register( wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) @@ -66,6 +71,7 @@ def burned_register_extrinsic( netuid: int, wait_for_inclusion: bool = False, wait_for_finalization: bool = True, + period: Optional[int] = None, ) -> bool: """Registers the wallet to chain by recycling TAO. @@ -77,6 +83,9 @@ def burned_register_extrinsic( 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 ``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout. + 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. Returns: success (bool): Flag is ``True`` if extrinsic was finalized or included in the block. If we did not wait for @@ -120,6 +129,7 @@ def burned_register_extrinsic( wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) if not success: @@ -154,6 +164,7 @@ def _do_pow_register( pow_result: "POWSolution", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, + period: Optional[int] = None, ) -> tuple[bool, Optional[str]]: """Sends a (POW) register extrinsic to the chain. @@ -164,6 +175,9 @@ def _do_pow_register( pow_result (POWSolution): The PoW result to register. wait_for_inclusion (bool): If ``True``, waits for the extrinsic to be included in a block. Default to `False`. wait_for_finalization (bool): If ``True``, waits for the extrinsic to be finalized. Default to `True`. + 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. Returns: success (bool): ``True`` if the extrinsic was included in a block. @@ -188,6 +202,7 @@ def _do_pow_register( wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) @@ -196,6 +211,7 @@ def register_subnet_extrinsic( wallet: "Wallet", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, + period: Optional[int] = None, ) -> bool: """ Registers a new subnetwork on the Bittensor blockchain. @@ -205,6 +221,9 @@ def register_subnet_extrinsic( wallet (Wallet): The wallet to be used for subnet registration. wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning true. wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning true. + 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. Returns: bool: True if the subnet registration was successful, False otherwise. @@ -232,16 +251,20 @@ def register_subnet_extrinsic( wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) + if not wait_for_finalization and not wait_for_inclusion: + return True + if success: logging.success( ":white_heavy_check_mark: [green]Successfully registered subnet[/green]" ) return True - else: - logging.error(f"Failed to register subnet: {message}") - return False + + logging.error(f"Failed to register subnet: {message}") + return False def register_extrinsic( @@ -258,6 +281,7 @@ def register_extrinsic( num_processes: Optional[int] = None, update_interval: Optional[int] = None, log_verbose: bool = False, + period: Optional[int] = None, ) -> bool: """Registers the wallet to the chain. @@ -277,6 +301,9 @@ def register_extrinsic( num_processes: The number of processes to use to register. update_interval: The number of nonces to solve between updates. log_verbose: If `True`, the registration process will log more information. + 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. Returns: `True` if extrinsic was finalized or included in the block. If we did not wait for finalization/inclusion, the @@ -366,7 +393,7 @@ def register_extrinsic( # pow successful, proceed to submit pow to chain for registration else: logging.info(":satellite: [magenta]Submitting POW...[/magenta]") - # check if pow result is still valid + # check if a pow result is still valid while not pow_result.is_stale(subtensor=subtensor): result: tuple[bool, Optional[str]] = _do_pow_register( subtensor=subtensor, @@ -375,6 +402,7 @@ def register_extrinsic( pow_result=pow_result, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) success, err_msg = result @@ -439,6 +467,7 @@ def set_subnet_identity_extrinsic( additional: str, wait_for_inclusion: bool = False, wait_for_finalization: bool = True, + period: Optional[int] = None, ) -> tuple[bool, str]: """ Set the identity information for a given subnet. @@ -456,6 +485,9 @@ def set_subnet_identity_extrinsic( additional (str): Any additional metadata or information related to the subnet. wait_for_inclusion (bool): Whether to wait for the extrinsic inclusion in a block (default: False). wait_for_finalization (bool): Whether to wait for the extrinsic finalization in a block (default: True). + 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. Returns: tuple[bool, str]: A tuple where the first element indicates success or failure (True/False), and the second @@ -487,6 +519,7 @@ def set_subnet_identity_extrinsic( wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) if not wait_for_finalization and not wait_for_inclusion: diff --git a/bittensor/core/extrinsics/root.py b/bittensor/core/extrinsics/root.py index 43fd65a5a1..f59a1104d7 100644 --- a/bittensor/core/extrinsics/root.py +++ b/bittensor/core/extrinsics/root.py @@ -1,5 +1,5 @@ import time -from typing import Union, TYPE_CHECKING +from typing import Optional, Union, TYPE_CHECKING import numpy as np from numpy.typing import NDArray @@ -16,6 +16,7 @@ from bittensor.utils.weight_utils import ( normalize_max_weight, convert_weights_and_uids_for_emit, + convert_uids_and_weights, ) if TYPE_CHECKING: @@ -50,8 +51,9 @@ def root_register_extrinsic( wallet: "Wallet", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, + period: Optional[int] = None, ) -> bool: - """Registers the wallet to root network. + """Registers the wallet to the root network. Arguments: subtensor (bittensor.core.subtensor.Subtensor): The Subtensor object @@ -60,6 +62,9 @@ def root_register_extrinsic( `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 `True`, or returns `False` if the extrinsic fails to be finalized within the timeout. + 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. Returns: `True` if extrinsic was finalized or included in the block. If we did not wait for finalization/inclusion, the @@ -78,7 +83,7 @@ def root_register_extrinsic( block=block, ) balance = subtensor.get_balance( - wallet.coldkeypub.ss58_address, + address=wallet.coldkeypub.ss58_address, block=block, ) @@ -114,10 +119,11 @@ def root_register_extrinsic( call_params={"hotkey": wallet.hotkey.ss58_address}, ) success, err_msg = subtensor.sign_and_send_extrinsic( - call, + call=call, wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) if not success: @@ -152,7 +158,7 @@ def _do_set_root_weights( version_key: int = 0, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, - period: int = 5, + period: Optional[int] = 8, ) -> tuple[bool, str]: """ Sets the root weights on the Subnet for the given wallet hotkey account. @@ -172,7 +178,9 @@ def _do_set_root_weights( False. wait_for_finalization (bool, optional): If True, waits for the extrinsic to be finalized on the chain. Defaults to False. - period (int, optional): The period in seconds to wait for extrinsic inclusion or finalization. Defaults to 5. + 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. Returns: tuple: Returns a tuple containing a boolean indicating success and a message describing the result of the @@ -190,28 +198,23 @@ def _do_set_root_weights( }, ) - next_nonce = subtensor.substrate.get_account_next_index(wallet.hotkey.ss58_address) - - # Period dictates how long the extrinsic will stay as part of waiting pool - extrinsic = subtensor.substrate.create_signed_extrinsic( + success, message = subtensor.sign_and_send_extrinsic( call=call, - keypair=wallet.coldkey, - era={"period": period}, - nonce=next_nonce, - ) - response = subtensor.substrate.submit_extrinsic( - extrinsic=extrinsic, + wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + use_nonce=True, + period=period, ) + # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: return True, "Not waiting for finalization or inclusion." - if response.is_success: + if success: return True, "Successfully set weights." - return False, format_error_message(response.error_message) + return False, message def set_root_weights_extrinsic( @@ -222,20 +225,24 @@ def set_root_weights_extrinsic( version_key: int = 0, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, + period: Optional[int] = None, ) -> bool: - """Sets the given weights and values on chain for wallet hotkey account. + """Sets the given weights and values on chain for a given wallet hotkey account. Arguments: subtensor (bittensor.core.subtensor.Subtensor): The Subtensor object wallet (bittensor_wallet.Wallet): Bittensor wallet object. netuids (Union[NDArray[np.int64], list[int]]): The `netuid` of the subnet to set weights for. - weights (Union[NDArray[np.float32], list[float]]): Weights to set. These must be `float` s and must correspond + weights (Union[NDArray[np.float32], list[float]]): Weights to set. These must be floats and must correspond to the passed `netuid` s. version_key (int): The version key of the validator. 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 True`, or returns `False` if the extrinsic fails to be finalized within the timeout. + 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. Returns: `True` if extrinsic was finalized or included in the block. If we did not wait for finalization/inclusion, the @@ -253,13 +260,10 @@ def set_root_weights_extrinsic( logging.error(unlock.message) return False - # First convert types. - if isinstance(netuids, list): - netuids = np.array(netuids, dtype=np.int64) - if isinstance(weights, list): - weights = np.array(weights, dtype=np.float32) + # Convert types. + netuids, weights = convert_uids_and_weights(netuids, weights) - logging.debug("Fetching weight limits") + logging.debug("[magenta]Fetching weight limits ...[/magenta]") min_allowed_weights, max_weight_limit = _get_limits(subtensor) # Get non zero values. @@ -273,7 +277,7 @@ def set_root_weights_extrinsic( ) # Normalize the weights to max value. - logging.info("Normalizing weights") + logging.info("[magenta]Normalizing weights ...[/magenta]") formatted_weights = normalize_max_weight(x=weights, limit=max_weight_limit) logging.info( f"Raw weights -> Normalized weights: [blue]{weights}[/blue] -> [green]{formatted_weights}[/green]" @@ -283,7 +287,7 @@ def set_root_weights_extrinsic( logging.info(":satellite: [magenta]Setting root weights...[magenta]") weight_uids, weight_vals = convert_weights_and_uids_for_emit(netuids, weights) - success, error_message = _do_set_root_weights( + success, message = _do_set_root_weights( subtensor=subtensor, wallet=wallet, netuids=weight_uids, @@ -291,18 +295,15 @@ def set_root_weights_extrinsic( version_key=version_key, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) - if not wait_for_finalization and not wait_for_inclusion: - return True - if success is True: logging.info(":white_heavy_check_mark: [green]Finalized[/green]") return True - else: - fmt_err = error_message - logging.error(f":cross_mark: [red]Failed error:[/red] {fmt_err}") - return False + + logging.error(f":cross_mark: [red]Failed error:[/red] {message}") + return False except SubstrateRequestException as e: fmt_err = format_error_message(e) diff --git a/bittensor/core/extrinsics/serving.py b/bittensor/core/extrinsics/serving.py index aaed6ad38f..d004fd7dba 100644 --- a/bittensor/core/extrinsics/serving.py +++ b/bittensor/core/extrinsics/serving.py @@ -2,14 +2,13 @@ from bittensor.core.errors import MetadataError from bittensor.core.settings import version_as_int +from bittensor.core.types import AxonServeCallParams from bittensor.utils import ( - format_error_message, networking as net, unlock_key, Certificate, ) from bittensor.utils.btlogging import logging -from bittensor.core.types import AxonServeCallParams if TYPE_CHECKING: from bittensor_wallet import Wallet @@ -23,7 +22,8 @@ def do_serve_axon( call_params: "AxonServeCallParams", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, -) -> tuple[bool, Optional[dict]]: + period: Optional[int] = None, +) -> tuple[bool, str]: """ Internal method to submit a serve axon transaction to the Bittensor blockchain. This method creates and submits a transaction, enabling a neuron's ``Axon`` to serve requests on the network. @@ -34,6 +34,9 @@ def do_serve_axon( call_params (bittensor.core.types.AxonServeCallParams): Parameters required for the serve axon call. wait_for_inclusion (bool): Waits for the transaction to be included in a block. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + 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. Returns: tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. @@ -51,21 +54,15 @@ def do_serve_axon( call_function=call_function, call_params=call_params.dict(), ) - extrinsic = subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.hotkey - ) - response = subtensor.substrate.submit_extrinsic( - extrinsic=extrinsic, + + success, message = subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) - if wait_for_inclusion or wait_for_finalization: - if response.is_success: - return True, None - - return False, response.error_message - - return True, None + return success, message def serve_extrinsic( @@ -80,6 +77,7 @@ def serve_extrinsic( wait_for_inclusion: bool = False, wait_for_finalization=True, certificate: Optional[Certificate] = None, + period: Optional[int] = None, ) -> bool: """Subscribes a Bittensor endpoint to the subtensor chain. @@ -98,6 +96,9 @@ def serve_extrinsic( ``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout. certificate (bittensor.utils.Certificate): Certificate to use for TLS. If ``None``, no TLS will be used. Defaults to ``None``. + 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. Returns: success (bool): Flag is ``True`` if extrinsic was finalized or included in the block. If we did not wait for @@ -130,33 +131,33 @@ def serve_extrinsic( neuron_up_to_date = not neuron.is_null and params == neuron if neuron_up_to_date: logging.debug( - f"Axon already served on: AxonInfo({wallet.hotkey.ss58_address},{ip}:{port}) " + f"Axon already served on: [blue]AxonInfo({wallet.hotkey.ss58_address}, {ip}:{port})[/blue]" ) return True logging.debug( - f"Serving axon with: AxonInfo({wallet.hotkey.ss58_address},{ip}:{port}) -> {subtensor.network}:{netuid}" + f"Serving axon with: [blue]AxonInfo({wallet.hotkey.ss58_address}, {ip}:{port})[/blue] -> " + f"[green]{subtensor.network}:{netuid}[/green]" ) - success, error_message = do_serve_axon( + success, message = do_serve_axon( subtensor=subtensor, wallet=wallet, call_params=params, wait_for_finalization=wait_for_finalization, wait_for_inclusion=wait_for_inclusion, + period=period, ) - if wait_for_inclusion or wait_for_finalization: - if success is True: - logging.debug( - f"Axon served with: AxonInfo({wallet.hotkey.ss58_address},{ip}:{port}) on {subtensor.network}:{netuid} " - ) - return True - else: - logging.error(f"Failed: {format_error_message(error_message)}") - return False - else: + if success: + logging.debug( + f"Axon served with: [blue]AxonInfo({wallet.hotkey.ss58_address}, {ip}:{port})[/blue] on " + f"[green]{subtensor.network}:{netuid}[/green]" + ) return True + logging.error(f"Failed: {message}") + return False + def serve_axon_extrinsic( subtensor: "Subtensor", @@ -165,6 +166,7 @@ def serve_axon_extrinsic( wait_for_inclusion: bool = False, wait_for_finalization: bool = True, certificate: Optional["Certificate"] = None, + period: Optional[int] = None, ) -> bool: """Serves the axon to the network. @@ -178,6 +180,9 @@ def serve_axon_extrinsic( ``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout. certificate (bittensor.utils.Certificate): Certificate to use for TLS. If ``None``, no TLS will be used. Defaults to ``None``. + 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. Returns: success (bool): Flag is ``True`` if extrinsic was finalized or included in the block. If we did not wait for @@ -213,6 +218,7 @@ def serve_axon_extrinsic( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, certificate=certificate, + period=period, ) return serve_success @@ -225,6 +231,7 @@ def publish_metadata( data: Union[bytes, dict], wait_for_inclusion: bool = False, wait_for_finalization: bool = True, + period: Optional[int] = None, ) -> bool: """ Publishes metadata on the Bittensor network using the specified wallet and network identifier. @@ -242,12 +249,15 @@ def publish_metadata( block before returning. Defaults to ``False``. wait_for_finalization (bool, optional): If ``True``, the function will wait for the extrinsic to be finalized on the chain before returning. Defaults to ``True``. + 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. Returns: bool: ``True`` if the metadata was successfully published (and finalized if specified). ``False`` otherwise. Raises: - MetadataError: If there is an error in submitting the extrinsic or if the response from the blockchain indicates + MetadataError: If there is an error in submitting the extrinsic, or if the response from the blockchain indicates failure. """ @@ -264,21 +274,17 @@ def publish_metadata( }, ) - extrinsic = subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.hotkey - ) - response = subtensor.substrate.submit_extrinsic( - extrinsic=extrinsic, + success, message = subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - if response.is_success: + if success: return True - raise MetadataError(format_error_message(response.error_message)) + raise MetadataError(message) def get_metadata( diff --git a/bittensor/core/extrinsics/set_weights.py b/bittensor/core/extrinsics/set_weights.py index 908fb2b2a9..039dbdf837 100644 --- a/bittensor/core/extrinsics/set_weights.py +++ b/bittensor/core/extrinsics/set_weights.py @@ -6,8 +6,11 @@ from numpy.typing import NDArray from bittensor.core.settings import version_as_int -from bittensor.utils import format_error_message, weight_utils from bittensor.utils.btlogging import logging +from bittensor.utils.weight_utils import ( + convert_and_normalize_weights_and_uids, + convert_uids_and_weights, +) if TYPE_CHECKING: from bittensor.core.subtensor import Subtensor @@ -24,8 +27,8 @@ def _do_set_weights( version_key: int = version_as_int, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, - period: int = 5, -) -> tuple[bool, Optional[str]]: # (success, error_message) + period: Optional[int] = None, +) -> tuple[bool, str]: """ Internal method to send a transaction to the Bittensor blockchain, setting weights for specified neurons. This method constructs and submits the transaction, handling @@ -40,10 +43,12 @@ def _do_set_weights( version_key (int, optional): Version key for compatibility with the network. wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. wait_for_finalization (bool, optional): Waits for the transaction to be finalized on the blockchain. - period (int, optional): The period in seconds to wait for extrinsic inclusion or finalization. Defaults to 5. + 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. Returns: - Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. + Tuple[bool, str]: A tuple containing a success flag and an optional error message. This method is vital for the dynamic weighting mechanism in Bittensor, where neurons adjust their trust in other neurons based on observed performance and contributions. @@ -59,27 +64,17 @@ def _do_set_weights( "version_key": version_key, }, ) - next_nonce = subtensor.substrate.get_account_next_index(wallet.hotkey.ss58_address) - # Period dictates how long the extrinsic will stay as part of waiting pool - extrinsic = subtensor.substrate.create_signed_extrinsic( + success, message = subtensor.sign_and_send_extrinsic( call=call, - keypair=wallet.hotkey, - era={"period": period}, - nonce=next_nonce, - ) - response = subtensor.substrate.submit_extrinsic( - extrinsic=extrinsic, + wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, + use_nonce=True, + nonce_key="hotkey", + sign_with="hotkey", ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True, "Not waiting for finalization or inclusion." - - if response.is_success: - return True, "Successfully set weights." - - return False, format_error_message(response.error_message) + return success, message def set_weights_extrinsic( @@ -91,9 +86,9 @@ def set_weights_extrinsic( version_key: int = 0, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, - period: int = 5, + period: Optional[int] = 8, ) -> tuple[bool, str]: - """Sets the given weights and values on chain for wallet hotkey account. + """Sets the given weights and values on a chain for a wallet hotkey account. Args: subtensor (bittensor.core.async_subtensor.AsyncSubtensor): Bittensor subtensor object. @@ -107,28 +102,27 @@ def set_weights_extrinsic( 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 ``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout. - period (int, optional): The period in seconds to wait for extrinsic inclusion or finalization. Defaults to 5. + 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. Returns: 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``. """ - # First convert types. - if isinstance(uids, list): - uids = np.array(uids, dtype=np.int64) - if isinstance(weights, list): - weights = np.array(weights, dtype=np.float32) + # Convert types. + uids, weights = convert_uids_and_weights(uids, weights) # Reformat and normalize. - weight_uids, weight_vals = weight_utils.convert_weights_and_uids_for_emit( - uids, weights - ) + weight_uids, weight_vals = convert_and_normalize_weights_and_uids(uids, weights) logging.info( - f":satellite: [magenta]Setting weights on [/magenta][blue]{subtensor.network}[/blue] [magenta]...[/magenta]" + f":satellite: [magenta]Setting weights on [/magenta]" + f"[blue]{subtensor.network}[/blue] " + f"[magenta]...[/magenta]" ) try: - success, error_message = _do_set_weights( + success, message = _do_set_weights( subtensor=subtensor, wallet=wallet, netuid=netuid, @@ -141,15 +135,15 @@ def set_weights_extrinsic( ) if not wait_for_finalization and not wait_for_inclusion: - return True, "Not waiting for finalization or inclusion." + return True, message if success is True: message = "Successfully set weights and Finalized." logging.success(f":white_heavy_check_mark: [green]{message}[/green]") return True, message - logging.error(f"[red]Failed[/red] set weights. Error: {error_message}") - return False, error_message + logging.error(f"[red]Failed[/red] set weights. Error: {message}") + return False, message except Exception as error: logging.error(f":cross_mark: [red]Failed[/red] set weights. Error: {error}") diff --git a/bittensor/core/extrinsics/staking.py b/bittensor/core/extrinsics/staking.py index 3a011bdf9b..58f5ed0b71 100644 --- a/bittensor/core/extrinsics/staking.py +++ b/bittensor/core/extrinsics/staking.py @@ -1,8 +1,9 @@ from typing import Optional, TYPE_CHECKING, Sequence -from bittensor.core.errors import StakeError, NotRegisteredError +from async_substrate_interface.errors import SubstrateRequestException + from bittensor.core.extrinsics.utils import get_old_stakes -from bittensor.utils import unlock_key +from bittensor.utils import unlock_key, format_error_message from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging @@ -22,6 +23,7 @@ def add_stake_extrinsic( safe_staking: bool = False, allow_partial_stake: bool = False, rate_tolerance: float = 0.005, + period: Optional[int] = None, ) -> bool: """ Adds the specified amount of stake to passed hotkey `uid`. @@ -29,7 +31,7 @@ def add_stake_extrinsic( Arguments: subtensor: the Subtensor object to use wallet: Bittensor wallet object. - hotkey_ss58: The `ss58` address of the hotkey account to stake to defaults to the wallet's hotkey. + hotkey_ss58: The `ss58` address of the hotkey account to stake to default to the wallet's hotkey. netuid (Optional[int]): Subnet unique ID. amount: Amount to stake as Bittensor balance, `None` if staking all. wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns @@ -39,10 +41,16 @@ def add_stake_extrinsic( safe_staking (bool): If true, enables price safety checks allow_partial_stake (bool): If true, allows partial unstaking if price tolerance exceeded rate_tolerance (float): Maximum allowed price increase percentage (0.005 = 0.5%) + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. Returns: success: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for finalization/inclusion, the response is `True`. + + Raises: + SubstrateRequestException: Raised if the extrinsic fails to be included in the block within the timeout. """ # Decrypt keys, @@ -143,16 +151,17 @@ def add_stake_extrinsic( call_params=call_params, ) - staking_response, err_msg = subtensor.sign_and_send_extrinsic( - call, - wallet, - wait_for_inclusion, - wait_for_finalization, + success, message = subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, use_nonce=True, sign_with="coldkey", nonce_key="coldkeypub", + period=period, ) - if staking_response is True: # If we successfully staked. + if success is True: # If we successfully staked. # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: return True @@ -160,8 +169,8 @@ def add_stake_extrinsic( logging.success(":white_heavy_check_mark: [green]Finalized[/green]") logging.info( - f":satellite: [magenta]Checking Balance on:[/magenta] [blue]{subtensor.network}[/blue] " - "[magenta]...[/magenta]" + f":satellite: [magenta]Checking Balance on:[/magenta] " + f"[blue]{subtensor.network}[/blue] [magenta]...[/magenta]" ) new_block = subtensor.get_current_block() new_balance = subtensor.get_balance( @@ -181,25 +190,19 @@ def add_stake_extrinsic( ) return True else: - if safe_staking and "Custom error: 8" in err_msg: + if safe_staking and "Custom error: 8" in message: logging.error( ":cross_mark: [red]Failed[/red]: Price exceeded tolerance limit. Either increase price tolerance or enable partial staking." ) else: - logging.error(f":cross_mark: [red]Failed: {err_msg}.[/red]") + logging.error(f":cross_mark: [red]Failed: {message}.[/red]") return False - # TODO I don't think these are used. Maybe should just catch SubstrateRequestException? - except NotRegisteredError: + except SubstrateRequestException as error: logging.error( - ":cross_mark: [red]Hotkey: {} is not registered.[/red]".format( - wallet.hotkey_str - ) + f":cross_mark: [red]Add Stake Error: {format_error_message((error))}[/red]" ) return False - except StakeError as e: - logging.error(f":cross_mark: [red]Stake Error: {e}[/red]") - return False def add_stake_multiple_extrinsic( @@ -210,6 +213,7 @@ def add_stake_multiple_extrinsic( amounts: Optional[list[Balance]] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + period: Optional[int] = None, ) -> bool: """Adds stake to each ``hotkey_ss58`` in the list, using each amount, from a common coldkey. @@ -217,11 +221,15 @@ def add_stake_multiple_extrinsic( subtensor: The initialized SubtensorInterface object. wallet: Bittensor wallet object for the coldkey. hotkey_ss58s: List of hotkeys to stake to. + netuids: List of netuids to stake to. amounts: List of amounts to stake. If `None`, stake all to the first hotkey. wait_for_inclusion: 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: If set, waits for the extrinsic to be finalized on the chain before returning `True`, or returns `False` if the extrinsic fails to be finalized within the timeout. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. Returns: success: `True` if extrinsic was finalized or included in the block. `True` if any wallet was staked. If we did @@ -327,17 +335,18 @@ def add_stake_multiple_extrinsic( "netuid": netuid, }, ) - staking_response, err_msg = subtensor.sign_and_send_extrinsic( - call, - wallet, - wait_for_inclusion, - wait_for_finalization, + success, message = subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, use_nonce=True, nonce_key="coldkeypub", sign_with="coldkey", + period=period, ) - if staking_response is True: # If we successfully staked. + if success is True: # If we successfully staked. # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: @@ -374,17 +383,14 @@ def add_stake_multiple_extrinsic( break else: - logging.error(f":cross_mark: [red]Failed[/red]: {err_msg}") + logging.error(f":cross_mark: [red]Failed[/red]: {message}") continue - except NotRegisteredError: + except SubstrateRequestException as error: logging.error( - f":cross_mark: [red]Hotkey: {hotkey_ss58} is not registered.[/red]" + f":cross_mark: [red]Add Stake Multiple error: {format_error_message(error)}[/red]" ) continue - except StakeError as e: - logging.error(f":cross_mark: [red]Stake Error: {e}[/red]") - continue if successful_stakes != 0: logging.info( diff --git a/bittensor/core/extrinsics/start_call.py b/bittensor/core/extrinsics/start_call.py index d3a1d423ce..2788bb88f1 100644 --- a/bittensor/core/extrinsics/start_call.py +++ b/bittensor/core/extrinsics/start_call.py @@ -1,6 +1,6 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional -from bittensor.utils import unlock_key, format_error_message +from bittensor.utils import unlock_key from bittensor.utils.btlogging import logging if TYPE_CHECKING: @@ -14,6 +14,7 @@ def start_call_extrinsic( netuid: int, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + period: Optional[int] = None, ) -> tuple[bool, str]: """ Submits a start_call extrinsic to the blockchain, to trigger the start call process for a subnet (used to start a @@ -25,6 +26,9 @@ def start_call_extrinsic( netuid (int): The UID of the target subnet for which the call is being initiated. wait_for_inclusion (bool, optional): Whether to wait for the extrinsic to be included in a block. Defaults to True. wait_for_finalization (bool, optional): Whether to wait for finalization of the extrinsic. Defaults to False. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. Returns: Tuple[bool, str]: @@ -35,28 +39,24 @@ def start_call_extrinsic( logging.error(unlock.message) return False, unlock.message - with subtensor.substrate as substrate: - start_call = substrate.compose_call( - call_module="SubtensorModule", - call_function="start_call", - call_params={"netuid": netuid}, - ) + start_call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="start_call", + call_params={"netuid": netuid}, + ) - signed_ext = substrate.create_signed_extrinsic( - call=start_call, - keypair=wallet.coldkey, - ) + success, message = subtensor.sign_and_send_extrinsic( + call=start_call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + ) - response = substrate.submit_extrinsic( - extrinsic=signed_ext, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) + if not wait_for_finalization and not wait_for_inclusion: + return True, message - if not wait_for_finalization and not wait_for_inclusion: - return True, "Not waiting for finalization or inclusion." + if success: + return True, "Success with `start_call` response." - if response.is_success: - return True, "Success with `start_call` response." - - return False, format_error_message(response.error_message) + return True, message diff --git a/bittensor/core/extrinsics/take.py b/bittensor/core/extrinsics/take.py index 1ac6e96040..968338d8a9 100644 --- a/bittensor/core/extrinsics/take.py +++ b/bittensor/core/extrinsics/take.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from bittensor_wallet.bittensor_wallet import Wallet @@ -16,7 +16,25 @@ def increase_take_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = True, raise_error: bool = False, + period: Optional[int] = None, ) -> tuple[bool, str]: + """Sets the delegate 'take' percentage for a neuron identified by its hotkey. + + Args: + subtensor (Subtensor): Blockchain connection. + wallet (Wallet): The wallet to sign the extrinsic. + hotkey_ss58 (str): SS58 address of the hotkey to set take for. + take (int): The percentage of rewards that the delegate claims from nominators. + wait_for_inclusion (bool, optional): Wait for inclusion before returning. Defaults to True. + wait_for_finalization (bool, optional): Wait for finalization before returning. Defaults to True. + raise_error (bool, optional): Raise error on failure. Defaults to False. + period: The number of blocks during which the transaction will remain valid after it's submitted. If + the transaction is not included in a block within that number of blocks, it will expire and be rejected. + You can think of it as an expiration date for the transaction. + + Returns: + tuple[bool, str]: Success flag and status message. + """ unlock = unlock_key(wallet, raise_error=raise_error) if not unlock.success: @@ -32,10 +50,11 @@ def increase_take_extrinsic( ) return subtensor.sign_and_send_extrinsic( - call, - wallet, + call=call, + wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, raise_error=raise_error, ) @@ -48,7 +67,25 @@ def decrease_take_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = True, raise_error: bool = False, + period: Optional[int] = None, ) -> tuple[bool, str]: + """Sets the delegate `take` percentage for a neuron identified by its hotkey. + + Args: + subtensor (Subtensor): Blockchain connection. + wallet (Wallet): The wallet to sign the extrinsic. + hotkey_ss58 (str): SS58 address of the hotkey to set take for. + take (int): The percentage of rewards that the delegate claims from nominators. + wait_for_inclusion (bool, optional): Wait for inclusion before returning. Defaults to True. + wait_for_finalization (bool, optional): Wait for finalization before returning. Defaults to True. + raise_error (bool, optional): Raise error on failure. Defaults to False. + 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. + + Returns: + tuple[bool, str]: Success flag and status message. + """ unlock = unlock_key(wallet, raise_error=raise_error) if not unlock.success: @@ -64,9 +101,10 @@ def decrease_take_extrinsic( ) return subtensor.sign_and_send_extrinsic( - call, - wallet, + call=call, + wallet=wallet, wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, raise_error=raise_error, + wait_for_finalization=wait_for_finalization, + period=period, ) diff --git a/bittensor/core/extrinsics/transfer.py b/bittensor/core/extrinsics/transfer.py index badc4d7cf9..03624097d0 100644 --- a/bittensor/core/extrinsics/transfer.py +++ b/bittensor/core/extrinsics/transfer.py @@ -1,13 +1,12 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from bittensor.core.settings import NETWORK_EXPLORER_MAP -from bittensor.utils.balance import Balance from bittensor.utils import ( is_valid_bittensor_address_or_public_key, unlock_key, get_explorer_url_for_network, - format_error_message, ) +from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging if TYPE_CHECKING: @@ -22,6 +21,7 @@ def _do_transfer( amount: Balance, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + period: Optional[int] = None, ) -> tuple[bool, str, str]: """ Makes transfer from wallet to destination public key address. @@ -33,8 +33,11 @@ def _do_transfer( amount (bittensor.utils.balance.Balance): Amount to stake as Bittensor balance. 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 + wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning `True`, or returns `False` if the extrinsic fails to be finalized within the timeout. + 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. Returns: success, block hash, formatted error message @@ -44,24 +47,25 @@ def _do_transfer( call_function="transfer_allow_death", call_params={"dest": destination, "value": amount.rao}, ) - extrinsic = subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = subtensor.substrate.submit_extrinsic( - extrinsic=extrinsic, + + success, message = subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) + # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: - return True, "", "Success, extrinsic submitted without waiting." + return True, "", message # Otherwise continue with finalization. - if response.is_success: - block_hash_ = response.block_hash + if success: + block_hash_ = subtensor.get_block_hash() return True, block_hash_, "Success with response." - return False, "", format_error_message(response.error_message) + return False, "", message def transfer_extrinsic( @@ -73,6 +77,7 @@ def transfer_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, keep_alive: bool = True, + period: Optional[int] = None, ) -> bool: """Transfers funds from this wallet to the destination public key address. @@ -84,21 +89,24 @@ def transfer_extrinsic( 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. - wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning + wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning `True`, or returns `False` if the extrinsic fails to be finalized within the timeout. keep_alive (bool): If set, keeps the account alive by keeping the balance above the existential deposit. + 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. Returns: 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. """ - destination = dest # Validate destination address. - if not is_valid_bittensor_address_or_public_key(destination): + if not is_valid_bittensor_address_or_public_key(dest): logging.error( - f":cross_mark: [red]Invalid destination SS58 address[/red]: {destination}" + f":cross_mark: [red]Invalid destination SS58 address[/red]: {dest}" ) return False + logging.info(f"Initiating transfer on network: {subtensor.network}") # Unlock wallet coldkey. if not (unlock := unlock_key(wallet)).success: @@ -119,7 +127,7 @@ def transfer_extrinsic( else: existential_deposit = subtensor.get_existential_deposit(block=block) - fee = subtensor.get_transfer_fee(wallet=wallet, dest=destination, value=amount) + fee = subtensor.get_transfer_fee(wallet=wallet, dest=dest, value=amount) # Check if we have enough balance. if transfer_all is True: @@ -139,10 +147,11 @@ def transfer_extrinsic( success, block_hash, err_msg = _do_transfer( subtensor=subtensor, wallet=wallet, - destination=destination, + destination=dest, amount=amount, wait_for_finalization=wait_for_finalization, wait_for_inclusion=wait_for_inclusion, + period=period, ) if success: @@ -168,6 +177,6 @@ def transfer_extrinsic( f"Balance: [blue]{account_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" ) return True - else: - logging.error(f":cross_mark: [red]Failed[/red]: {err_msg}") - return False + + logging.error(f":cross_mark: [red]Failed[/red]: {err_msg}") + return False diff --git a/bittensor/core/extrinsics/unstaking.py b/bittensor/core/extrinsics/unstaking.py index f49362a4db..34fe47d7ac 100644 --- a/bittensor/core/extrinsics/unstaking.py +++ b/bittensor/core/extrinsics/unstaking.py @@ -1,8 +1,9 @@ from typing import Optional, TYPE_CHECKING -from bittensor.core.errors import StakeError, NotRegisteredError +from async_substrate_interface.errors import SubstrateRequestException + from bittensor.core.extrinsics.utils import get_old_stakes -from bittensor.utils import unlock_key +from bittensor.utils import unlock_key, format_error_message from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging @@ -22,6 +23,7 @@ def unstake_extrinsic( safe_staking: bool = False, allow_partial_stake: bool = False, rate_tolerance: float = 0.005, + period: Optional[int] = None, ) -> bool: """Removes stake into the wallet coldkey from the specified hotkey ``uid``. @@ -39,6 +41,9 @@ def unstake_extrinsic( safe_staking: If true, enables price safety checks allow_partial_stake: If true, allows partial unstaking if price tolerance exceeded rate_tolerance: Maximum allowed price decrease percentage (0.005 = 0.5%) + 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. Returns: success (bool): Flag is ``True`` if extrinsic was finalized or included in the block. If we did not wait for @@ -64,6 +69,7 @@ def unstake_extrinsic( block=block, ) + # Covert to bittensor.Balance if amount is None: # Unstake it all. logging.warning( @@ -131,17 +137,18 @@ def unstake_extrinsic( call_params=call_params, ) - staking_response, err_msg = subtensor.sign_and_send_extrinsic( - call, - wallet, - wait_for_inclusion, - wait_for_finalization, + success, message = subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, nonce_key="coldkeypub", sign_with="coldkey", use_nonce=True, + period=period, ) - if staking_response is True: # If we successfully unstaked. + if success is True: # If we successfully unstaked. # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: return True @@ -170,22 +177,19 @@ def unstake_extrinsic( ) return True else: - if safe_staking and "Custom error: 8" in err_msg: + if safe_staking and "Custom error: 8" in message: logging.error( ":cross_mark: [red]Failed[/red]: Price exceeded tolerance limit. Either increase price tolerance or enable partial staking." ) else: - logging.error(f":cross_mark: [red]Failed: {err_msg}.[/red]") + logging.error(f":cross_mark: [red]Failed: {message}.[/red]") return False - except NotRegisteredError: + except SubstrateRequestException as error: logging.error( - f":cross_mark: [red]Hotkey: {wallet.hotkey_str} is not registered.[/red]" + f":cross_mark: [red]Unstake filed with error: {format_error_message(error)}[/red]" ) return False - except StakeError as e: - logging.error(f":cross_mark: [red]Stake Error: {e}[/red]") - return False def unstake_multiple_extrinsic( @@ -196,6 +200,7 @@ def unstake_multiple_extrinsic( amounts: Optional[list[Balance]] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + period: Optional[int] = None, ) -> bool: """Removes stake from each ``hotkey_ss58`` in the list, using each amount, to a common coldkey. @@ -209,6 +214,9 @@ def unstake_multiple_extrinsic( 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 ``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout. + 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. Returns: success (bool): Flag is ``True`` if extrinsic was finalized or included in the block. Flag is ``True`` if any @@ -298,13 +306,14 @@ def unstake_multiple_extrinsic( }, ) staking_response, err_msg = subtensor.sign_and_send_extrinsic( - call, - wallet, - wait_for_inclusion, - wait_for_finalization, + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, nonce_key="coldkeypub", sign_with="coldkey", use_nonce=True, + period=period, ) if staking_response is True: # If we successfully unstaked. @@ -334,14 +343,11 @@ def unstake_multiple_extrinsic( logging.error(f":cross_mark: [red]Failed: {err_msg}.[/red]") continue - except NotRegisteredError: + except SubstrateRequestException as error: logging.error( - f":cross_mark: [red]Hotkey[/red] [blue]{hotkey_ss58}[/blue] [red]is not registered.[/red]" + f":cross_mark: [red]Multiple unstake filed with error: {format_error_message(error)}[/red]" ) - continue - except StakeError as e: - logging.error(":cross_mark: [red]Stake Error: {}[/red]".format(e)) - continue + return False if successful_unstakes != 0: logging.info( diff --git a/bittensor/core/extrinsics/utils.py b/bittensor/core/extrinsics/utils.py index 7cc2e3c4e0..7566aa9bc2 100644 --- a/bittensor/core/extrinsics/utils.py +++ b/bittensor/core/extrinsics/utils.py @@ -2,98 +2,11 @@ from typing import TYPE_CHECKING -from async_substrate_interface.errors import SubstrateRequestException - -from bittensor.utils import format_error_message from bittensor.utils.balance import Balance -from bittensor.utils.btlogging import logging if TYPE_CHECKING: from bittensor_wallet import Wallet - from bittensor.core.async_subtensor import AsyncSubtensor - from async_substrate_interface import ( - AsyncExtrinsicReceipt, - ExtrinsicReceipt, - ) - from bittensor.core.subtensor import Subtensor from bittensor.core.chain_data import StakeInfo - from scalecodec.types import GenericExtrinsic - - -def submit_extrinsic( - subtensor: "Subtensor", - extrinsic: "GenericExtrinsic", - wait_for_inclusion: bool, - wait_for_finalization: bool, -) -> "ExtrinsicReceipt": - """ - Submits an extrinsic to the substrate blockchain and handles potential exceptions. - - This function attempts to submit an extrinsic to the substrate blockchain with specified options - for waiting for inclusion in a block and/or finalization. If an exception occurs during submission, - it logs the error and re-raises the exception. - - Args: - subtensor: The Subtensor instance used to interact with the blockchain. - extrinsic (scalecodec.types.GenericExtrinsic): The extrinsic to be submitted to the blockchain. - wait_for_inclusion (bool): Whether to wait for the extrinsic to be included in a block. - wait_for_finalization (bool): Whether to wait for the extrinsic to be finalized on the blockchain. - - Returns: - response: The response from the substrate after submitting the extrinsic. - - Raises: - SubstrateRequestException: If the submission of the extrinsic fails, the error is logged and re-raised. - """ - try: - return subtensor.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - except SubstrateRequestException as e: - logging.error(format_error_message(e.args[0])) - # Re-raise the exception for retrying of the extrinsic call. If we remove the retry logic, - # the raise will need to be removed. - raise - - -async def async_submit_extrinsic( - subtensor: "AsyncSubtensor", - extrinsic: "GenericExtrinsic", - wait_for_inclusion: bool, - wait_for_finalization: bool, -) -> "AsyncExtrinsicReceipt": - """ - Submits an extrinsic to the substrate blockchain and handles potential exceptions. - - This function attempts to submit an extrinsic to the substrate blockchain with specified options - for waiting for inclusion in a block and/or finalization. If an exception occurs during submission, - it logs the error and re-raises the exception. - - Args: - subtensor: The AsyncSubtensor instance used to interact with the blockchain. - extrinsic: The extrinsic to be submitted to the blockchain. - wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. - wait_for_finalization: Whether to wait for the extrinsic to be finalized on the blockchain. - - Returns: - response: The response from the substrate after submitting the extrinsic. - - Raises: - SubstrateRequestException: If the submission of the extrinsic fails, the error is logged and re-raised. - """ - try: - return await subtensor.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - except SubstrateRequestException as e: - logging.error(format_error_message(e.args[0])) - # Re-raise the exception for retrying of the extrinsic call. If we remove the retry logic, - # the raise will need to be removed. - raise def get_old_stakes( @@ -101,19 +14,19 @@ def get_old_stakes( hotkey_ss58s: list[str], netuids: list[int], all_stakes: list["StakeInfo"], -) -> list[Balance]: +) -> list["Balance"]: """ Retrieve the previous staking balances for a wallet's hotkeys across given netuids. - This function searches through the provided staking data to find the stake amounts - for the specified hotkeys and netuids associated with the wallet's coldkey. If no match - is found for a particular hotkey and netuid combination, a default balance of zero is returned. + This function searches through the provided staking data to find the stake amounts for the specified hotkeys and + netuids associated with the wallet's coldkey. If no match is found for a particular hotkey and netuid combination, + a default balance of zero is returned. Args: - wallet (Wallet): The wallet containing the coldkey to compare with stake data. - hotkey_ss58s (list[str]): List of hotkey SS58 addresses for which stakes are retrieved. - netuids (list[int]): List of network unique identifiers (netuids) corresponding to the hotkeys. - all_stakes (list[StakeInfo]): A collection of all staking information to search through. + wallet: The wallet containing the coldkey to compare with stake data. + hotkey_ss58s: List of hotkey SS58 addresses for which stakes are retrieved. + netuids: List of network unique identifiers (netuids) corresponding to the hotkeys. + all_stakes: A collection of all staking information to search through. Returns: list[Balance]: A list of Balances, each representing the stake for a given hotkey and netuid. diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index f7626297ca..63634d9053 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -6,25 +6,26 @@ import numpy as np import scalecodec from async_substrate_interface.errors import SubstrateRequestException +from async_substrate_interface.substrate_addons import RetrySyncSubstrate from async_substrate_interface.sync_substrate import SubstrateInterface from async_substrate_interface.types import ScaleObj -from bittensor_commit_reveal import get_encrypted_commitment +from bittensor_drand import get_encrypted_commitment from numpy.typing import NDArray from bittensor.core.async_subtensor import ProposalVoteData from bittensor.core.axon import Axon from bittensor.core.chain_data import ( + DelegatedInfo, DelegateInfo, DynamicInfo, MetagraphInfo, NeuronInfo, NeuronInfoLite, StakeInfo, + SubnetInfo, + SubnetIdentity, SubnetHyperparameters, WeightCommitInfo, - SubnetIdentity, - SubnetInfo, - DelegatedInfo, decode_account_id, ) from bittensor.core.chain_data.chain_identity import ChainIdentity @@ -60,12 +61,12 @@ get_metadata, serve_axon_extrinsic, ) -from bittensor.core.extrinsics.start_call import start_call_extrinsic from bittensor.core.extrinsics.set_weights import set_weights_extrinsic from bittensor.core.extrinsics.staking import ( add_stake_extrinsic, add_stake_multiple_extrinsic, ) +from bittensor.core.extrinsics.start_call import start_call_extrinsic from bittensor.core.extrinsics.take import ( decrease_take_extrinsic, increase_take_extrinsic, @@ -100,7 +101,7 @@ check_and_convert_to_balance, ) from bittensor.utils.btlogging import logging -from bittensor.utils.weight_utils import generate_weight_hash +from bittensor.utils.weight_utils import generate_weight_hash, convert_uids_and_weights if TYPE_CHECKING: from bittensor_wallet import Wallet @@ -109,14 +110,16 @@ class Subtensor(SubtensorMixin): - """Thin layer for interacting with Substrate Interface. Mostly a collection of frequently-used calls.""" + """Thin layer for interacting with Substrate Interface. Mostly a collection of frequently used calls.""" def __init__( self, network: Optional[str] = None, config: Optional["Config"] = None, - _mock: bool = False, log_verbose: bool = False, + fallback_chains: Optional[list[str]] = None, + retry_forever: bool = False, + _mock: bool = False, ): """ Initializes an instance of the Subtensor class. @@ -124,8 +127,10 @@ def __init__( Arguments: network (str): The network name or type to connect to. config (Optional[Config]): Configuration object for the AsyncSubtensor instance. - _mock: Whether this is a mock instance. Mainly just for use in testing. log_verbose (bool): Enables or disables verbose logging. + fallback_chains (list): List of fallback chains endpoints to use if no network is specified. Defaults to `None`. + retry_forever (bool): Whether to retry forever on connection errors. Defaults to `False`. + _mock: Whether this is a mock instance. Mainly just for use in testing. Raises: Any exceptions raised during the setup, configuration, or connection process. @@ -134,7 +139,6 @@ def __init__( config = self.config() self._config = copy.deepcopy(config) self.chain_endpoint, self.network = self.setup_config(network, self._config) - self._mock = _mock self.log_verbose = log_verbose self._check_and_log_network_settings() @@ -143,13 +147,8 @@ def __init__( f"Connecting to network: [blue]{self.network}[/blue], " f"chain_endpoint: [blue]{self.chain_endpoint}[/blue]> ..." ) - self.substrate = SubstrateInterface( - url=self.chain_endpoint, - ss58_format=SS58_FORMAT, - type_registry=TYPE_REGISTRY, - use_remote_preset=True, - chain_name="Bittensor", - _mock=_mock, + self.substrate = self._get_substrate( + fallback_chains=fallback_chains, retry_forever=retry_forever, _mock=_mock ) if self.log_verbose: logging.info( @@ -163,11 +162,45 @@ def __exit__(self, exc_type, exc_val, exc_tb): self.close() def close(self): - """ - Closes the websocket connection - """ + """Closes the websocket connection.""" self.substrate.close() + def _get_substrate( + self, + fallback_chains: Optional[list[str]] = None, + retry_forever: bool = False, + _mock: bool = False, + ) -> Union[SubstrateInterface, RetrySyncSubstrate]: + """Creates the Substrate instance based on provided arguments. + + Arguments: + fallback_chains (list): List of fallback chains endpoints to use if no network is specified. Defaults to `None`. + retry_forever (bool): Whether to retry forever on connection errors. Defaults to `False`. + _mock: Whether this is a mock instance. Mainly just for use in testing. + + Returns: + the instance of the SubstrateInterface or RetrySyncSubstrate class. + """ + if fallback_chains or retry_forever: + return RetrySyncSubstrate( + url=self.chain_endpoint, + ss58_format=SS58_FORMAT, + type_registry=TYPE_REGISTRY, + use_remote_preset=True, + chain_name="Bittensor", + fallback_chains=fallback_chains, + retry_forever=retry_forever, + _mock=_mock, + ) + return SubstrateInterface( + url=self.chain_endpoint, + ss58_format=SS58_FORMAT, + type_registry=TYPE_REGISTRY, + use_remote_preset=True, + chain_name="Bittensor", + _mock=_mock, + ) + # Subtensor queries =========================================================================================== def query_constant( @@ -387,6 +420,23 @@ def all_subnets(self, block: Optional[int] = None) -> Optional[list["DynamicInfo ) return DynamicInfo.list_from_dicts(query.decode()) + def blocks_since_last_step( + self, netuid: int, block: Optional[int] = None + ) -> Optional[int]: + """Returns number of blocks since the last epoch of the subnet. + + Arguments: + netuid (int): The unique identifier of the subnetwork. + block: the block number for this query. + + Returns: + block number of the last step in the subnet. + """ + query = self.query_subtensor( + name="BlocksSinceLastStep", block=block, params=[netuid] + ) + return query.value if query is not None and hasattr(query, "value") else query + def blocks_since_last_update(self, netuid: int, uid: int) -> Optional[int]: """ Returns the number of blocks since the last update for a specific UID in the subnetwork. @@ -435,7 +485,9 @@ def bonds( return b_map - def commit(self, wallet, netuid: int, data: str) -> bool: + def commit( + self, wallet, netuid: int, data: str, period: Optional[int] = None + ) -> bool: """ Commits arbitrary data to the Bittensor network by publishing metadata. @@ -443,6 +495,12 @@ def commit(self, wallet, netuid: int, data: str) -> bool: wallet (bittensor_wallet.Wallet): The wallet associated with the neuron committing the data. netuid (int): The unique identifier of the subnetwork. data (str): The data to be committed to the network. + 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. + + Returns: + bool: `True` if the commitment was successful, `False` otherwise. """ return publish_metadata( subtensor=self, @@ -450,6 +508,7 @@ def commit(self, wallet, netuid: int, data: str) -> bool: netuid=netuid, data_type=f"Raw{len(data)}", data=data.encode(), + period=period, ) # add explicit alias @@ -459,7 +518,7 @@ def commit_reveal_enabled( self, netuid: int, block: Optional[int] = None ) -> Optional[bool]: """ - Check if commit-reveal mechanism is enabled for a given network at a specific block. + Check if the commit-reveal mechanism is enabled for a given network at a specific block. Arguments: netuid: The network identifier for which to check the commit-reveal mechanism. @@ -1321,8 +1380,12 @@ def get_next_epoch_start_block( int: The block number at which the next epoch will start. """ block = block or self.block + blocks_since_last_step = self.blocks_since_last_step(netuid=netuid, block=block) tempo = self.tempo(netuid=netuid, block=block) - return (((block // tempo) + 1) * tempo) + 1 if tempo else None + + if block and blocks_since_last_step and tempo: + return block - blocks_since_last_step + tempo + 1 + return None def get_owned_hotkeys( self, @@ -1878,150 +1941,9 @@ def immunity_period( ) return None if call is None else int(call) - def set_children( - self, - wallet: "Wallet", - hotkey: str, - netuid: int, - children: list[tuple[float, str]], - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - raise_error: bool = False, - ) -> tuple[bool, str]: - """ - Allows a coldkey to set children keys. - - Arguments: - wallet (bittensor_wallet.Wallet): bittensor wallet instance. - hotkey (str): The ``SS58`` address of the neuron's hotkey. - netuid (int): The netuid value. - children (list[tuple[float, str]]): A list of children with their proportions. - wait_for_inclusion (bool): Waits for the transaction to be included in a block. - wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. - raise_error: Raises relevant exception rather than returning `False` if unsuccessful. - - Returns: - tuple[bool, str]: A tuple where the first element is a boolean indicating success or failure of the - operation, and the second element is a message providing additional information. - - Raises: - DuplicateChild: There are duplicates in the list of children. - InvalidChild: Child is the hotkey. - NonAssociatedColdKey: The coldkey does not own the hotkey or the child is the same as the hotkey. - NotEnoughStakeToSetChildkeys: Parent key doesn't have minimum own stake. - ProportionOverflow: The sum of the proportions does exceed uint64. - RegistrationNotPermittedOnRootSubnet: Attempting to register a child on the root network. - SubNetworkDoesNotExist: Attempting to register to a non-existent network. - TooManyChildren: Too many children in request. - TxRateLimitExceeded: Hotkey hit the rate limit. - bittensor_wallet.errors.KeyFileError: Failed to decode keyfile data. - bittensor_wallet.errors.PasswordError: Decryption failed or wrong password for decryption provided. - """ - - unlock = unlock_key(wallet, raise_error=raise_error) - - if not unlock.success: - return False, unlock.message - - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="set_children", - call_params={ - "children": [ - ( - float_to_u64(proportion), - child_hotkey, - ) - for proportion, child_hotkey in children - ], - "hotkey": hotkey, - "netuid": netuid, - }, - ) - - return self.sign_and_send_extrinsic( - call, - wallet, - wait_for_inclusion, - wait_for_finalization, - raise_error=raise_error, - ) - - def set_delegate_take( - self, - wallet: "Wallet", - hotkey_ss58: str, - take: float, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - raise_error: bool = False, - ) -> tuple[bool, str]: - """ - Sets the delegate 'take' percentage for a nueron identified by its hotkey. - The 'take' represents the percentage of rewards that the delegate claims from its nominators' stakes. - - Arguments: - wallet (bittensor_wallet.Wallet): bittensor wallet instance. - hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. - take (float): Percentage reward for the delegate. - wait_for_inclusion (bool): Waits for the transaction to be included in a block. - wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. - raise_error: Raises relevant exception rather than returning `False` if unsuccessful. - - Returns: - tuple[bool, str]: A tuple where the first element is a boolean indicating success or failure of the - operation, and the second element is a message providing additional information. - - Raises: - DelegateTakeTooHigh: Delegate take is too high. - DelegateTakeTooLow: Delegate take is too low. - DelegateTxRateLimitExceeded: A transactor exceeded the rate limit for delegate transaction. - HotKeyAccountNotExists: The hotkey does not exists. - NonAssociatedColdKey: Request to stake, unstake or subscribe is made by a coldkey that is not associated with the hotkey account. - bittensor_wallet.errors.PasswordError: Decryption failed or wrong password for decryption provided. - bittensor_wallet.errors.KeyFileError: Failed to decode keyfile data. - - The delegate take is a critical parameter in the network's incentive structure, influencing the distribution of - rewards among neurons and their nominators. - """ - - # u16 representation of the take - take_u16 = int(take * 0xFFFF) - - current_take = self.get_delegate_take(hotkey_ss58) - current_take_u16 = int(current_take * 0xFFFF) - - if current_take_u16 == take_u16: - logging.info(":white_heavy_check_mark: [green]Already Set[/green]") - return True, "" - - logging.info(f"Updating {hotkey_ss58} take: current={current_take} new={take}") - - if current_take_u16 < take_u16: - success, error = increase_take_extrinsic( - self, - wallet, - hotkey_ss58, - take_u16, - wait_for_finalization=wait_for_finalization, - wait_for_inclusion=wait_for_inclusion, - raise_error=raise_error, - ) - else: - success, error = decrease_take_extrinsic( - self, - wallet, - hotkey_ss58, - take_u16, - wait_for_finalization=wait_for_finalization, - wait_for_inclusion=wait_for_inclusion, - raise_error=raise_error, - ) - - if success: - logging.info(":white_heavy_check_mark: [green]Take Updated[/green]") - - return success, error + def is_fast_blocks(self): + """Returns True if the node is running with fast blocks. False if not.""" + return self.query_constant("SubtensorModule", "DurationOfStartCall").value == 10 def is_hotkey_delegate(self, hotkey_ss58: str, block: Optional[int] = None) -> bool: """ @@ -2322,6 +2244,7 @@ def set_reveal_commitment( data: str, blocks_until_reveal: int = 360, block_time: Union[int, float] = 12, + period: Optional[int] = None, ) -> tuple[bool, int]: """ Commits arbitrary data to the Bittensor network by publishing metadata. @@ -2330,10 +2253,12 @@ def set_reveal_commitment( wallet (bittensor_wallet.Wallet): The wallet associated with the neuron committing the data. netuid (int): The unique identifier of the subnetwork. data (str): The data to be committed to the network. - blocks_until_reveal (int): The number of blocks from now after which the data will be revealed. Defaults to `360`. - Then amount of blocks in one epoch. + blocks_until_reveal (int): The number of blocks from now after which the data will be revealed. Defaults to + `360`. Then number of blocks in one epoch. block_time (Union[int, float]): The number of seconds between each block. Defaults to `12`. - + 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. Returns: bool: `True` if the commitment was successful, `False` otherwise. @@ -2353,6 +2278,7 @@ def set_reveal_commitment( netuid=netuid, data_type=f"TimelockEncrypted", data=data_, + period=period, ), reveal_round def subnet(self, netuid: int, block: Optional[int] = None) -> Optional[DynamicInfo]: @@ -2456,14 +2382,14 @@ def wait_for_block(self, block: Optional[int] = None): waits for the next block. Args: - block (Optional[int]): The block number to wait for. If None, waits for next block. + block (Optional[int]): The block number to wait for. If None, waits for the next block. Returns: bool: True if the target block was reached, False if timeout occurred. Example: - >>> subtensor.wait_for_block() # Waits for next block - >>> subtensor.wait_for_block(block=1234) # Waits for specific block + >>> subtensor.wait_for_block() # Waits for the next block + >>> subtensor.wait_for_block(block=1234) # Waits for a specific block """ def handler(block_data: dict): @@ -2472,6 +2398,7 @@ def handler(block_data: dict): ) if block_data["header"]["number"] >= target_block: return True + return None current_block = self.substrate.get_block() current_block_hash = current_block.get("header", {}).get("hash") @@ -2545,6 +2472,46 @@ def get_timestamp(self, block: Optional[int] = None) -> datetime: unix = cast(ScaleObj, self.query_module("Timestamp", "Now", block=block)).value return datetime.fromtimestamp(unix / 1000, tz=timezone.utc) + def get_subnet_owner_hotkey( + self, netuid: int, block: Optional[int] = None + ) -> Optional[str]: + """ + Retrieves the hotkey of the subnet owner for a given network UID. + + This function queries the subtensor network to fetch the hotkey of the owner of a subnet specified by its + netuid. If no data is found or the query fails, the function returns None. + + Arguments: + netuid: The network UID of the subnet to fetch the owner's hotkey for. + block: The specific block number to query the data from. + + Returns: + The hotkey of the subnet owner if available; None otherwise. + """ + return self.query_subtensor( + name="SubnetOwnerHotkey", params=[netuid], block=block + ) + + def get_subnet_validator_permits( + self, netuid: int, block: Optional[int] = None + ) -> Optional[list[bool]]: + """ + Retrieves the list of validator permits for a given subnet as boolean values. + + Arguments: + netuid: The unique identifier of the subnetwork. + block: The blockchain block number for the query. + + Returns: + A list of boolean values representing validator permits, or None if not available. + """ + query = self.query_subtensor( + name="ValidatorPermit", + params=[netuid], + block=block, + ) + return query.value if query is not None and hasattr(query, "value") else query + # Extrinsics helper ================================================================================================ def sign_and_send_extrinsic( @@ -2567,11 +2534,19 @@ def sign_and_send_extrinsic( wallet (bittensor_wallet.Wallet): the wallet whose coldkey will be used to sign the extrinsic wait_for_inclusion (bool): whether to wait until the extrinsic call is included on the chain wait_for_finalization (bool): whether to wait until the extrinsic call is finalized on the chain - sign_with: the wallet's keypair to use for the signing. Options are "coldkey", "hotkey", "coldkeypub" - raise_error: raises relevant exception rather than returning `False` if unsuccessful. + sign_with (str): the wallet's keypair to use for the signing. Options are "coldkey", "hotkey", "coldkeypub" + use_nonce (bool): unique identifier for the transaction related with hot/coldkey. + 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. + nonce_key: the type on nonce to use. Options are "hotkey" or "coldkey". + raise_error: raises the relevant exception rather than returning `False` if unsuccessful. Returns: (success, error message) + + Raises: + SubstrateRequestException: Substrate request exception. """ possible_keys = ("coldkey", "hotkey", "coldkeypub") if sign_with not in possible_keys: @@ -2602,7 +2577,9 @@ def sign_and_send_extrinsic( ) # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: - return True, "" + message = f"Not waiting for finalization or inclusion." + logging.debug(f"{message}. Extrinsic: {extrinsic}") + return True, message if response.is_success: return True, "" @@ -2631,6 +2608,7 @@ def add_stake( safe_staking: bool = False, allow_partial_stake: bool = False, rate_tolerance: float = 0.005, + period: Optional[int] = None, ) -> bool: """ Adds the specified amount of stake to a neuron identified by the hotkey ``SS58`` address. @@ -2651,6 +2629,9 @@ def add_stake( exceed the tolerance. Default is False. rate_tolerance (float): The maximum allowed price change ratio when staking. For example, 0.005 = 0.5% maximum price increase. Only used when safe_staking is True. Default is 0.005. + 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. Returns: bool: True if the staking is successful, False otherwise. @@ -2672,6 +2653,7 @@ def add_stake( safe_staking=safe_staking, allow_partial_stake=allow_partial_stake, rate_tolerance=rate_tolerance, + period=period, ) def add_stake_multiple( @@ -2682,6 +2664,7 @@ def add_stake_multiple( amounts: Optional[list[Balance]] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + period: Optional[int] = None, ) -> bool: """ Adds stakes to multiple neurons identified by their hotkey SS58 addresses. @@ -2694,6 +2677,9 @@ def add_stake_multiple( amounts (list[Balance]): Corresponding amounts of TAO to stake for each hotkey. wait_for_inclusion (bool): Waits for the transaction to be included in a block. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + 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. Returns: bool: ``True`` if the staking is successful for all specified neurons, False otherwise. @@ -2709,6 +2695,7 @@ def add_stake_multiple( amounts=amounts, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) def burned_register( @@ -2717,6 +2704,7 @@ def burned_register( netuid: int, wait_for_inclusion: bool = False, wait_for_finalization: bool = True, + period: Optional[int] = None, ) -> bool: """ Registers a neuron on the Bittensor network by recycling TAO. This method of registration involves recycling @@ -2729,6 +2717,9 @@ def burned_register( `False`. wait_for_finalization (bool, optional): Waits for the transaction to be finalized on the blockchain. Defaults to `True`. + 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. Returns: bool: ``True`` if the registration is successful, False otherwise. @@ -2740,6 +2731,7 @@ def burned_register( wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) return burned_register_extrinsic( @@ -2748,6 +2740,7 @@ def burned_register( netuid=netuid, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) def commit_weights( @@ -2761,6 +2754,7 @@ def commit_weights( wait_for_inclusion: bool = False, wait_for_finalization: bool = False, max_retries: int = 5, + period: Optional[int] = 16, ) -> tuple[bool, str]: """ Commits a hash of the neuron's weights to the Bittensor blockchain using the provided wallet. @@ -2773,15 +2767,19 @@ def commit_weights( uids (np.ndarray): NumPy array of neuron UIDs for which weights are being committed. weights (np.ndarray): NumPy array of weight values corresponding to each UID. version_key (int): Version key for compatibility with the network. Default is ``int representation of - Bittensor version.``. + a Bittensor version.``. wait_for_inclusion (bool): Waits for the transaction to be included in a block. Default is ``False``. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Default is ``False``. max_retries (int): The number of maximum attempts to commit weights. Default is ``5``. + 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. Returns: - tuple[bool, str]: ``True`` if the weight commitment is successful, False otherwise. And `msg`, a string - value describing the success or potential error. + tuple[bool, str]: + `True` if the weight commitment is successful, False otherwise. + `msg` is a string value describing the success or potential error. This function allows neurons to create a tamper-proof record of their weight distribution at a specific point in time, enhancing transparency and accountability within the Bittensor network. @@ -2791,8 +2789,9 @@ def commit_weights( message = "No attempt made. Perhaps it is too soon to commit weights!" logging.info( - f"Committing weights with params: netuid={netuid}, uids={uids}, weights={weights}, " - f"version_key={version_key}" + f"Committing weights with params: " + f"netuid=[blue]{netuid}[/blue], uids=[blue]{uids}[/blue], weights=[blue]{weights}[/blue], " + f"version_key=[blue]{version_key}[/blue]" ) # Generate the hash of the weights @@ -2814,12 +2813,12 @@ def commit_weights( commit_hash=commit_hash, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) if success: break except Exception as e: logging.error(f"Error committing weights: {e}") - finally: retries += 1 return success, message @@ -2834,6 +2833,7 @@ def move_stake( amount: Balance, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + period: Optional[int] = None, ) -> bool: """ Moves stake to a different hotkey and/or subnet. @@ -2847,6 +2847,9 @@ def move_stake( amount (Balance): Amount of stake to move. wait_for_inclusion (bool): Waits for the transaction to be included in a block. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + 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. Returns: success (bool): True if the stake movement was successful. @@ -2862,6 +2865,7 @@ def move_stake( amount=amount, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) def register( @@ -2878,6 +2882,7 @@ def register( num_processes: Optional[int] = None, update_interval: Optional[int] = None, log_verbose: bool = False, + period: Optional[int] = None, ) -> bool: """ Registers a neuron on the Bittensor network using the provided wallet. @@ -2900,6 +2905,9 @@ def register( num_processes (Optional[int]): The number of processes to use to register. Default to `None`. update_interval (Optional[int]): The number of nonces to solve between updates. Default to `None`. log_verbose (bool): If ``true``, the registration process will log more information. Default to `False`. + 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. Returns: bool: ``True`` if the registration is successful, False otherwise. @@ -2921,6 +2929,7 @@ def register( dev_id=dev_id, output_in_place=output_in_place, log_verbose=log_verbose, + period=period, ) def register_subnet( @@ -2928,6 +2937,7 @@ def register_subnet( wallet: "Wallet", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, + period: Optional[int] = None, ) -> bool: """ Registers a new subnetwork on the Bittensor network. @@ -2938,6 +2948,9 @@ def register_subnet( false if the extrinsic fails to enter the block within the timeout. Default is False. wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning true, or returns false if the extrinsic fails to be finalized within the timeout. Default is True. + 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. Returns: bool: True if the subnet registration was successful, False otherwise. @@ -2947,6 +2960,7 @@ def register_subnet( wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) def reveal_weights( @@ -2960,6 +2974,7 @@ def reveal_weights( wait_for_inclusion: bool = False, wait_for_finalization: bool = False, max_retries: int = 5, + period: Optional[int] = 16, ) -> tuple[bool, str]: """ Reveals the weights for a specific subnet on the Bittensor blockchain using the provided wallet. @@ -2972,11 +2987,14 @@ def reveal_weights( weights (np.ndarray): NumPy array of weight values corresponding to each UID. salt (np.ndarray): NumPy array of salt values corresponding to the hash function. version_key (int): Version key for compatibility with the network. Default is ``int representation of - Bittensor version``. + the Bittensor version``. wait_for_inclusion (bool): Waits for the transaction to be included in a block. Default is ``False``. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Default is ``False``. max_retries (int): The number of maximum attempts to reveal weights. Default is ``5``. + 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. Returns: tuple[bool, str]: ``True`` if the weight revelation is successful, False otherwise. And `msg`, a string @@ -3001,12 +3019,12 @@ def reveal_weights( version_key=version_key, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) if success: break except Exception as e: logging.error(f"Error revealing weights: {e}") - finally: retries += 1 return success, message @@ -3016,6 +3034,7 @@ def root_register( wallet: "Wallet", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, + period: Optional[int] = None, ) -> bool: """ Register neuron by recycling some TAO. @@ -3025,6 +3044,9 @@ def root_register( wait_for_inclusion (bool): Waits for the transaction to be included in a block. Default is ``False``. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Default is ``False``. + 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. Returns: `True` if registration was successful, otherwise `False`. @@ -3035,6 +3057,7 @@ def root_register( wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) def root_set_weights( @@ -3045,9 +3068,10 @@ def root_set_weights( version_key: int = 0, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, + period: Optional[int] = None, ) -> bool: """ - Set weights for root network. + Set weights for the root network. Arguments: wallet (bittensor_wallet.Wallet): bittensor wallet instance. @@ -3058,12 +3082,14 @@ def root_set_weights( ``False``. wait_for_finalization (bool, optional): Waits for the transaction to be finalized on the blockchain. Defaults to ``False``. + 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. Returns: `True` if the setting of weights is successful, `False` otherwise. """ - netuids_ = np.array(netuids, dtype=np.int64) - weights_ = np.array(weights, dtype=np.float32) + netuids_, weights_ = convert_uids_and_weights(netuids, weights) logging.info(f"Setting weights in network: [blue]{self.network}[/blue]") return set_root_weights_extrinsic( subtensor=self, @@ -3073,8 +3099,165 @@ def root_set_weights( version_key=version_key, wait_for_finalization=wait_for_finalization, wait_for_inclusion=wait_for_inclusion, + period=period, + ) + + def set_children( + self, + wallet: "Wallet", + hotkey: str, + netuid: int, + children: list[tuple[float, str]], + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + raise_error: bool = False, + period: Optional[int] = None, + ) -> tuple[bool, str]: + """ + Allows a coldkey to set children-keys. + + Arguments: + wallet (bittensor_wallet.Wallet): bittensor wallet instance. + hotkey (str): The ``SS58`` address of the neuron's hotkey. + netuid (int): The netuid value. + children (list[tuple[float, str]]): A list of children with their proportions. + wait_for_inclusion (bool): Waits for the transaction to be included in a block. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + 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. + + Returns: + tuple[bool, str]: A tuple where the first element is a boolean indicating success or failure of the + operation, and the second element is a message providing additional information. + + Raises: + DuplicateChild: There are duplicates in the list of children. + InvalidChild: Child is the hotkey. + NonAssociatedColdKey: The coldkey does not own the hotkey or the child is the same as the hotkey. + NotEnoughStakeToSetChildkeys: Parent key doesn't have minimum own stake. + ProportionOverflow: The sum of the proportions does exceed uint64. + RegistrationNotPermittedOnRootSubnet: Attempting to register a child on the root network. + SubNetworkDoesNotExist: Attempting to register to a non-existent network. + TooManyChildren: Too many children in request. + TxRateLimitExceeded: Hotkey hit the rate limit. + bittensor_wallet.errors.KeyFileError: Failed to decode keyfile data. + bittensor_wallet.errors.PasswordError: Decryption failed or wrong password for decryption provided. + """ + + unlock = unlock_key(wallet, raise_error=raise_error) + + if not unlock.success: + return False, unlock.message + + call = self.substrate.compose_call( + call_module="SubtensorModule", + call_function="set_children", + call_params={ + "children": [ + ( + float_to_u64(proportion), + child_hotkey, + ) + for proportion, child_hotkey in children + ], + "hotkey": hotkey, + "netuid": netuid, + }, + ) + + return self.sign_and_send_extrinsic( + call, + wallet, + wait_for_inclusion, + wait_for_finalization, + raise_error=raise_error, + period=period, ) + def set_delegate_take( + self, + wallet: "Wallet", + hotkey_ss58: str, + take: float, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + raise_error: bool = False, + period: Optional[int] = None, + ) -> tuple[bool, str]: + """ + Sets the delegate 'take' percentage for a neuron identified by its hotkey. + The 'take' represents the percentage of rewards that the delegate claims from its nominators' stakes. + + Arguments: + wallet (bittensor_wallet.Wallet): bittensor wallet instance. + hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. + take (float): Percentage reward for the delegate. + wait_for_inclusion (bool): Waits for the transaction to be included in a block. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + 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. + + Returns: + tuple[bool, str]: A tuple where the first element is a boolean indicating success or failure of the + operation, and the second element is a message providing additional information. + + Raises: + DelegateTakeTooHigh: Delegate take is too high. + DelegateTakeTooLow: Delegate take is too low. + DelegateTxRateLimitExceeded: A transactor exceeded the rate limit for delegate transaction. + HotKeyAccountNotExists: The hotkey does not exist. + NonAssociatedColdKey: Request to stake, unstake, or subscribe is made by a coldkey that is not associated with the hotkey account. + bittensor_wallet.errors.PasswordError: Decryption failed or wrong password for decryption provided. + bittensor_wallet.errors.KeyFileError: Failed to decode keyfile data. + + The delegate take is a critical parameter in the network's incentive structure, influencing the distribution of + rewards among neurons and their nominators. + """ + + # u16 representation of the take + take_u16 = int(take * 0xFFFF) + + current_take = self.get_delegate_take(hotkey_ss58) + current_take_u16 = int(current_take * 0xFFFF) + + if current_take_u16 == take_u16: + logging.info(":white_heavy_check_mark: [green]Already Set[/green]") + return True, "" + + logging.info(f"Updating {hotkey_ss58} take: current={current_take} new={take}") + + if current_take_u16 < take_u16: + success, error = increase_take_extrinsic( + self, + wallet, + hotkey_ss58, + take_u16, + wait_for_finalization=wait_for_finalization, + wait_for_inclusion=wait_for_inclusion, + raise_error=raise_error, + period=period, + ) + else: + success, error = decrease_take_extrinsic( + self, + wallet, + hotkey_ss58, + take_u16, + wait_for_finalization=wait_for_finalization, + wait_for_inclusion=wait_for_inclusion, + raise_error=raise_error, + period=period, + ) + + if success: + logging.info(":white_heavy_check_mark: [green]Take Updated[/green]") + + return success, error + def set_subnet_identity( self, wallet: "Wallet", @@ -3082,6 +3265,7 @@ def set_subnet_identity( subnet_identity: SubnetIdentity, wait_for_inclusion: bool = False, wait_for_finalization: bool = True, + period: Optional[int] = None, ) -> tuple[bool, str]: """ Sets the identity of a subnet for a specific wallet and network. @@ -3093,6 +3277,9 @@ def set_subnet_identity( repository, contact, URL, discord, description, and any additional metadata. wait_for_inclusion (bool): Indicates if the function should wait for the transaction to be included in the block. wait_for_finalization (bool): Indicates if the function should wait for the transaction to reach finalization. + 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. Returns: tuple[bool, str]: A tuple where the first element is a boolean indicating success or failure of the @@ -3111,6 +3298,7 @@ def set_subnet_identity( additional=subnet_identity.additional, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) def set_weights( @@ -3124,7 +3312,7 @@ def set_weights( wait_for_finalization: bool = False, max_retries: int = 5, block_time: float = 12.0, - period: int = 5, + period: Optional[int] = 8, ) -> tuple[bool, str]: """ Sets the inter-neuronal weights for the specified neuron. This process involves specifying the influence or @@ -3132,27 +3320,26 @@ def set_weights( decentralized learning architecture. Arguments: - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron setting the weights. - netuid (int): The unique identifier of the subnet. - uids (Union[NDArray[np.int64], torch.LongTensor, list]): The list of neuron UIDs that the weights are being - set for. - weights (Union[NDArray[np.float32], torch.FloatTensor, list]): The corresponding weights to be set for each - UID. - version_key (int): Version key for compatibility with the network. Default is int representation of - Bittensor version. - wait_for_inclusion (bool): Waits for the transaction to be included in a block. Default is ``False``. - wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Default is - ``False``. - max_retries (int): The number of maximum attempts to set weights. Default is ``5``. - block_time (float): The amount of seconds for block duration. Default is 12.0 seconds. - period (int, optional): The period in seconds to wait for extrinsic inclusion or finalization. Defaults to 5. - - Returns: - tuple[bool, str]: ``True`` if the setting of weights is successful, False otherwise. And `msg`, a string - value describing the success or potential error. + wallet: The wallet associated with the neuron setting the weights. + netuid: The unique identifier of the subnet. + uids: The list of neuron UIDs that the weights are being set for. + weights: The corresponding weights to be set for each UID. + version_key: Version key for compatibility with the network. Default is int representation of a Bittensor version. + wait_for_inclusion: Waits for the transaction to be included in a block. Default is ``False``. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. Default is ``False``. + max_retries: The number of maximum attempts to set weights. Default is ``5``. + block_time: The number of seconds for block duration. Default is 12.0 seconds. + 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. Default is 16. + + Returns: + tuple: + ``True`` if the setting of weights is successful, False otherwise. + `msg` is a string value describing the success or potential error. This function is crucial in shaping the network's collective intelligence, where each neuron's learning and - contribution are influenced by the weights it sets towards others【81†source】. + contribution are influenced by the weights it sets towards others. """ def _blocks_weight_limit() -> bool: @@ -3176,7 +3363,8 @@ def _blocks_weight_limit() -> bool: while retries < max_retries and success is False and _blocks_weight_limit(): logging.info( - f"Committing weights for subnet #{netuid}. Attempt {retries + 1} of {max_retries}." + f"Committing weights for subnet [blue]{netuid}[/blue]. " + f"Attempt [blue]{retries + 1}[blue] of [green]{max_retries}[/green]." ) success, message = commit_reveal_v3_extrinsic( subtensor=self, @@ -3188,17 +3376,18 @@ def _blocks_weight_limit() -> bool: wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, block_time=block_time, + period=period, ) retries += 1 return success, message else: - # go with classic `set weights extrinsic` + # go with classic `set_weights_extrinsic` while retries < max_retries and success is False and _blocks_weight_limit(): try: logging.info( - f"Setting weights for subnet #[blue]{netuid}[/blue]. " - f"Attempt [blue]{retries + 1} of {max_retries}[/blue]." + f"Setting weights for subnet [blue]{netuid}[/blue]. " + f"Attempt [blue]{retries + 1}[/blue] of [green]{max_retries}[/green]." ) success, message = set_weights_extrinsic( subtensor=self, @@ -3213,7 +3402,6 @@ def _blocks_weight_limit() -> bool: ) except Exception as e: logging.error(f"Error setting weights: {e}") - finally: retries += 1 return success, message @@ -3225,6 +3413,7 @@ def serve_axon( wait_for_inclusion: bool = False, wait_for_finalization: bool = True, certificate: Optional[Certificate] = None, + period: Optional[int] = None, ) -> bool: """ Registers an ``Axon`` serving endpoint on the Bittensor network for a specific neuron. This function is used to @@ -3238,6 +3427,9 @@ def serve_axon( ``True``. certificate (bittensor.utils.Certificate): Certificate to use for TLS. If ``None``, no TLS will be used. Defaults to ``None``. + 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. Returns: bool: ``True`` if the Axon serve registration is successful, False otherwise. @@ -3252,6 +3444,7 @@ def serve_axon( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, certificate=certificate, + period=period, ) def start_call( @@ -3260,6 +3453,7 @@ def start_call( netuid: int, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + period: Optional[int] = None, ) -> tuple[bool, str]: """ Submits a start_call extrinsic to the blockchain, to trigger the start call process for a subnet (used to start a @@ -3270,6 +3464,9 @@ def start_call( netuid (int): The UID of the target subnet for which the call is being initiated. wait_for_inclusion (bool, optional): Whether to wait for the extrinsic to be included in a block. Defaults to True. wait_for_finalization (bool, optional): Whether to wait for finalization of the extrinsic. Defaults to False. + 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. Returns: Tuple[bool, str]: @@ -3282,6 +3479,7 @@ def start_call( netuid=netuid, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) def swap_stake( @@ -3296,6 +3494,7 @@ def swap_stake( safe_staking: bool = False, allow_partial_stake: bool = False, rate_tolerance: float = 0.005, + period: Optional[int] = None, ) -> bool: """ Moves stake between subnets while keeping the same coldkey-hotkey pair ownership. @@ -3318,7 +3517,9 @@ def swap_stake( rate_tolerance (float): The maximum allowed increase in the price ratio between subnets (origin_price/destination_price). For example, 0.005 = 0.5% maximum increase. Only used when safe_staking is True. Default is 0.005. - + 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. Returns: success (bool): True if the extrinsic was successful. @@ -3343,6 +3544,7 @@ def swap_stake( safe_staking=safe_staking, allow_partial_stake=allow_partial_stake, rate_tolerance=rate_tolerance, + period=period, ) def transfer( @@ -3354,6 +3556,7 @@ def transfer( wait_for_finalization: bool = False, transfer_all: bool = False, keep_alive: bool = True, + period: Optional[int] = None, ) -> bool: """ Transfer token of amount to destination. @@ -3361,12 +3564,15 @@ def transfer( Arguments: wallet (bittensor_wallet.Wallet): Source wallet for the transfer. dest (str): Destination address for the transfer. - amount (float): Amount of tokens to transfer. + amount (float): Amount of tao to transfer. transfer_all (bool): Flag to transfer all tokens. Default is ``False``. wait_for_inclusion (bool): Waits for the transaction to be included in a block. Default is ``True``. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Default is ``False``. keep_alive (bool): Flag to keep the connection alive. Default is ``True``. + 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. Returns: `True` if the transferring was successful, otherwise `False`. @@ -3381,6 +3587,7 @@ def transfer( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, keep_alive=keep_alive, + period=period, ) def transfer_stake( @@ -3393,6 +3600,7 @@ def transfer_stake( amount: Balance, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + period: Optional[int] = None, ) -> bool: """ Transfers stake from one subnet to another while changing the coldkey owner. @@ -3406,6 +3614,9 @@ def transfer_stake( amount (Union[Balance, float, int]): Amount to transfer. wait_for_inclusion (bool): If true, waits for inclusion before returning. wait_for_finalization (bool): If true, waits for finalization before returning. + 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. Returns: success (bool): True if the transfer was successful. @@ -3421,6 +3632,7 @@ def transfer_stake( amount=amount, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) def unstake( @@ -3434,6 +3646,7 @@ def unstake( safe_staking: bool = False, allow_partial_stake: bool = False, rate_tolerance: float = 0.005, + period: Optional[int] = None, ) -> bool: """ Removes a specified amount of stake from a single hotkey account. This function is critical for adjusting @@ -3444,7 +3657,7 @@ def unstake( removed. hotkey_ss58 (Optional[str]): The ``SS58`` address of the hotkey account to unstake from. netuid (Optional[int]): The unique identifier of the subnet. - amount (Balance): The amount of TAO to unstake. If not specified, unstakes all. + amount (Balance): The amount of alpha to unstake. If not specified, unstakes all. wait_for_inclusion (bool): Waits for the transaction to be included in a block. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. safe_staking (bool): If true, enables price safety checks to protect against fluctuating prices. The unstake @@ -3454,6 +3667,9 @@ def unstake( exceed the tolerance. Default is False. rate_tolerance (float): The maximum allowed price change ratio when unstaking. For example, 0.005 = 0.5% maximum price decrease. Only used when safe_staking is True. Default is 0.005. + 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. Returns: bool: ``True`` if the unstaking process is successful, False otherwise. @@ -3474,6 +3690,7 @@ def unstake( safe_staking=safe_staking, allow_partial_stake=allow_partial_stake, rate_tolerance=rate_tolerance, + period=period, ) def unstake_multiple( @@ -3484,6 +3701,7 @@ def unstake_multiple( amounts: Optional[list[Balance]] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + period: Optional[int] = None, ) -> bool: """ Performs batch unstaking from multiple hotkey accounts, allowing a neuron to reduce its staked amounts @@ -3498,6 +3716,9 @@ def unstake_multiple( unstakes all available stakes. wait_for_inclusion (bool): Waits for the transaction to be included in a block. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + 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. Returns: bool: ``True`` if the batch unstaking is successful, False otherwise. @@ -3513,4 +3734,5 @@ def unstake_multiple( amounts=amounts, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) diff --git a/bittensor/core/subtensor_api/__init__.py b/bittensor/core/subtensor_api/__init__.py new file mode 100644 index 0000000000..be5a61638a --- /dev/null +++ b/bittensor/core/subtensor_api/__init__.py @@ -0,0 +1,236 @@ +from typing import Optional, Union, TYPE_CHECKING + +from bittensor.core.async_subtensor import AsyncSubtensor as _AsyncSubtensor +from bittensor.core.subtensor import Subtensor as _Subtensor +from .chain import Chain as _Chain +from .commitments import Commitments as _Commitments +from .delegates import Delegates as _Delegates +from .extrinsics import Extrinsics as _Extrinsics +from .metagraphs import Metagraphs as _Metagraphs +from .neurons import Neurons as _Neurons +from .queries import Queries as _Queries +from .staking import Staking as _Staking +from .subnets import Subnets as _Subnets +from .utils import add_legacy_methods as _add_classic_fields +from .wallets import Wallets as _Wallets + +if TYPE_CHECKING: + from bittensor.core.config import Config + + +class SubtensorApi: + """Subtensor API class. + + Arguments: + network: The network to connect to. Defaults to `None` -> "finney". + config: Bittensor configuration object. Defaults to `None`. + legacy_methods: If `True`, all methods from the Subtensor class will be added to the root level of this class. + fallback_chains (list): List of fallback chains to use if no network is specified. Defaults to `None`. + retry_forever (bool): Whether to retry forever on connection errors. Defaults to `False`. + log_verbose (bool): Enables or disables verbose logging. + mock: Whether this is a mock instance. Mainly just for use in testing. + + Example: + # sync version + import bittensor as bt + + subtensor = bt.SubtensorApi() + print(subtensor.block) + print(subtensor.delegates.get_delegate_identities()) + subtensor.chain.tx_rate_limit() + + # async version + import bittensor as bt + + subtensor = bt.SubtensorApi(async_subtensor=True) + async with subtensor: + print(await subtensor.block) + print(await subtensor.delegates.get_delegate_identities()) + print(await subtensor.chain.tx_rate_limit()) + + # using `legacy_methods` + import bittensor as bt + + subtensor = bt.SubtensorApi(legacy_methods=True) + print(subtensor.bonds(0)) + + # using `fallback_chains` or `retry_forever` + import bittensor as bt + + + """ + + def __init__( + self, + network: Optional[str] = None, + config: Optional["Config"] = None, + async_subtensor: bool = False, + legacy_methods: bool = False, + fallback_chains: Optional[list[str]] = None, + retry_forever: bool = False, + log_verbose: bool = False, + mock: bool = False, + ): + self.network = network + self._fallback_chains = fallback_chains + self._retry_forever = retry_forever + self._mock = mock + self.log_verbose = log_verbose + self.is_async = async_subtensor + self._config = config + + # assigned only for async instance + self.initialize = None + self._subtensor = self._get_subtensor() + + # fix naming collision + self._neurons = _Neurons(self._subtensor) + + # define empty fields + self.substrate = self._subtensor.substrate + self.chain_endpoint = self._subtensor.chain_endpoint + self.close = self._subtensor.close + self.config = self._subtensor.config + self.setup_config = self._subtensor.setup_config + self.help = self._subtensor.help + + self.determine_block_hash = self._subtensor.determine_block_hash + self.encode_params = self._subtensor.encode_params + self.sign_and_send_extrinsic = self._subtensor.sign_and_send_extrinsic + self.start_call = self._subtensor.start_call + self.wait_for_block = self._subtensor.wait_for_block + + # adds all Subtensor methods into main level os SubtensorApi class + if legacy_methods: + _add_classic_fields(self) + + def _get_subtensor(self) -> Union["_Subtensor", "_AsyncSubtensor"]: + """Returns the subtensor instance based on the provided config and subtensor type flag.""" + if self.is_async: + _subtensor = _AsyncSubtensor( + network=self.network, + config=self._config, + log_verbose=self.log_verbose, + fallback_chains=self._fallback_chains, + retry_forever=self._retry_forever, + _mock=self._mock, + ) + self.initialize = _subtensor.initialize + return _subtensor + else: + return _Subtensor( + network=self.network, + config=self._config, + log_verbose=self.log_verbose, + fallback_chains=self._fallback_chains, + retry_forever=self._retry_forever, + _mock=self._mock, + ) + + def _determine_chain_endpoint(self) -> str: + """Determines the connection and mock flag.""" + if self._mock: + return "Mock" + return self.substrate.url + + def __str__(self): + return ( + f"" + ) + + def __repr__(self): + return self.__str__() + + def __enter__(self): + if self.is_async: + raise NotImplementedError( + "Async version of SubtensorApi cannot be used with sync context manager." + ) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if self.is_async: + raise NotImplementedError( + "Async version of SubtensorApi cannot be used with sync context manager." + ) + self.close() + + async def __aenter__(self): + if not self.is_async: + raise NotImplementedError( + "Sync version of SubtensorApi cannot be used with async context manager." + ) + return await self._subtensor.__aenter__() + + async def __aexit__(self, exc_type, exc_val, exc_tb): + if not self.is_async: + raise NotImplementedError( + "Sync version of SubtensorApi cannot be used with async context manager." + ) + await self.substrate.close() + + @classmethod + def add_args(cls, parser): + _Subtensor.add_args(parser) + + @property + def block(self): + """Returns current chain block number.""" + return self._subtensor.block + + @property + def chain(self): + """Property of interaction with chain methods.""" + return _Chain(self._subtensor) + + @property + def commitments(self): + """Property to access commitments methods.""" + return _Commitments(self._subtensor) + + @property + def delegates(self): + """Property to access delegates methods.""" + return _Delegates(self._subtensor) + + @property + def extrinsics(self): + """Property to access extrinsics methods.""" + return _Extrinsics(self._subtensor) + + @property + def metagraphs(self): + """Property to access metagraphs methods.""" + return _Metagraphs(self._subtensor) + + @property + def neurons(self): + """Property to access neurons methods.""" + return self._neurons + + @neurons.setter + def neurons(self, value): + """Setter for neurons property.""" + self._neurons = value + + @property + def queries(self): + """Property to access subtensor queries methods.""" + return _Queries(self._subtensor) + + @property + def staking(self): + """Property to access staking methods.""" + return _Staking(self._subtensor) + + @property + def subnets(self): + """Property of interaction with subnets methods.""" + return _Subnets(self._subtensor) + + @property + def wallets(self): + """Property of interaction methods with cold/hotkeys, and balances, etc.""" + return _Wallets(self._subtensor) diff --git a/bittensor/core/subtensor_api/chain.py b/bittensor/core/subtensor_api/chain.py new file mode 100644 index 0000000000..cd2bfda02f --- /dev/null +++ b/bittensor/core/subtensor_api/chain.py @@ -0,0 +1,20 @@ +from typing import Union +from bittensor.core.subtensor import Subtensor as _Subtensor +from bittensor.core.async_subtensor import AsyncSubtensor as _AsyncSubtensor + + +class Chain: + """Class for managing chain state operations.""" + + def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): + self.get_block_hash = subtensor.get_block_hash + self.get_current_block = subtensor.get_current_block + self.get_delegate_identities = subtensor.get_delegate_identities + self.get_existential_deposit = subtensor.get_existential_deposit + self.get_minimum_required_stake = subtensor.get_minimum_required_stake + self.get_vote_data = subtensor.get_vote_data + self.get_timestamp = subtensor.get_timestamp + self.is_fast_blocks = subtensor.is_fast_blocks + self.last_drand_round = subtensor.last_drand_round + self.state_call = subtensor.state_call + self.tx_rate_limit = subtensor.tx_rate_limit diff --git a/bittensor/core/subtensor_api/commitments.py b/bittensor/core/subtensor_api/commitments.py new file mode 100644 index 0000000000..2e594ba6db --- /dev/null +++ b/bittensor/core/subtensor_api/commitments.py @@ -0,0 +1,20 @@ +from typing import Union +from bittensor.core.subtensor import Subtensor as _Subtensor +from bittensor.core.async_subtensor import AsyncSubtensor as _AsyncSubtensor + + +class Commitments: + """Class for managing any commitment operations.""" + + def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): + self.commit_reveal_enabled = subtensor.commit_reveal_enabled + self.get_all_commitments = subtensor.get_all_commitments + 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_revealed_commitment = subtensor.get_revealed_commitment + self.get_revealed_commitment_by_hotkey = ( + subtensor.get_revealed_commitment_by_hotkey + ) + self.set_commitment = subtensor.set_commitment + self.set_reveal_commitment = subtensor.set_reveal_commitment diff --git a/bittensor/core/subtensor_api/delegates.py b/bittensor/core/subtensor_api/delegates.py new file mode 100644 index 0000000000..1cdbfca08c --- /dev/null +++ b/bittensor/core/subtensor_api/delegates.py @@ -0,0 +1,16 @@ +from typing import Union +from bittensor.core.subtensor import Subtensor as _Subtensor +from bittensor.core.async_subtensor import AsyncSubtensor as _AsyncSubtensor + + +class Delegates: + """Class for managing delegate operations.""" + + def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): + self.is_hotkey_delegate = subtensor.is_hotkey_delegate + self.get_delegate_by_hotkey = subtensor.get_delegate_by_hotkey + self.set_delegate_take = subtensor.set_delegate_take + self.get_delegate_identities = subtensor.get_delegate_identities + self.get_delegate_take = subtensor.get_delegate_take + self.get_delegated = subtensor.get_delegated + self.get_delegates = subtensor.get_delegates diff --git a/bittensor/core/subtensor_api/extrinsics.py b/bittensor/core/subtensor_api/extrinsics.py new file mode 100644 index 0000000000..0ff4439201 --- /dev/null +++ b/bittensor/core/subtensor_api/extrinsics.py @@ -0,0 +1,29 @@ +from typing import Union +from bittensor.core.subtensor import Subtensor as _Subtensor +from bittensor.core.async_subtensor import AsyncSubtensor as _AsyncSubtensor + + +class Extrinsics: + """Class for managing extrinsic operations.""" + + def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): + self.add_stake = subtensor.add_stake + self.add_stake_multiple = subtensor.add_stake_multiple + self.burned_register = subtensor.burned_register + self.commit_weights = subtensor.commit_weights + self.move_stake = subtensor.move_stake + self.register = subtensor.register + self.register_subnet = subtensor.register_subnet + self.reveal_weights = subtensor.reveal_weights + self.root_register = subtensor.root_register + self.root_set_weights = subtensor.root_set_weights + self.set_children = subtensor.set_children + self.set_subnet_identity = subtensor.set_subnet_identity + self.set_weights = subtensor.set_weights + self.serve_axon = subtensor.serve_axon + self.start_call = subtensor.start_call + self.swap_stake = subtensor.swap_stake + self.transfer = subtensor.transfer + self.transfer_stake = subtensor.transfer_stake + self.unstake = subtensor.unstake + self.unstake_multiple = subtensor.unstake_multiple diff --git a/bittensor/core/subtensor_api/metagraphs.py b/bittensor/core/subtensor_api/metagraphs.py new file mode 100644 index 0000000000..af143a1620 --- /dev/null +++ b/bittensor/core/subtensor_api/metagraphs.py @@ -0,0 +1,12 @@ +from typing import Union +from bittensor.core.subtensor import Subtensor as _Subtensor +from bittensor.core.async_subtensor import AsyncSubtensor as _AsyncSubtensor + + +class Metagraphs: + """Class for managing metagraph operations.""" + + def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): + self.get_metagraph_info = subtensor.get_metagraph_info + self.get_all_metagraphs_info = subtensor.get_all_metagraphs_info + self.metagraph = subtensor.metagraph diff --git a/bittensor/core/subtensor_api/neurons.py b/bittensor/core/subtensor_api/neurons.py new file mode 100644 index 0000000000..c1fd40066f --- /dev/null +++ b/bittensor/core/subtensor_api/neurons.py @@ -0,0 +1,15 @@ +from typing import Union +from bittensor.core.subtensor import Subtensor as _Subtensor +from bittensor.core.async_subtensor import AsyncSubtensor as _AsyncSubtensor + + +class Neurons: + """Class for managing neuron operations.""" + + def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): + self.get_all_neuron_certificates = subtensor.get_all_neuron_certificates + self.get_neuron_certificate = subtensor.get_neuron_certificate + self.neuron_for_uid = subtensor.neuron_for_uid + self.neurons = subtensor.neurons + self.neurons_lite = subtensor.neurons_lite + self.query_identity = subtensor.query_identity diff --git a/bittensor/core/subtensor_api/queries.py b/bittensor/core/subtensor_api/queries.py new file mode 100644 index 0000000000..7209ffb7ae --- /dev/null +++ b/bittensor/core/subtensor_api/queries.py @@ -0,0 +1,15 @@ +from typing import Union +from bittensor.core.subtensor import Subtensor as _Subtensor +from bittensor.core.async_subtensor import AsyncSubtensor as _AsyncSubtensor + + +class Queries: + """Class for managing subtensor query operations.""" + + def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): + self.query_constant = subtensor.query_constant + self.query_map = subtensor.query_map + self.query_map_subtensor = subtensor.query_map_subtensor + self.query_module = subtensor.query_module + self.query_runtime_api = subtensor.query_runtime_api + self.query_subtensor = subtensor.query_subtensor diff --git a/bittensor/core/subtensor_api/staking.py b/bittensor/core/subtensor_api/staking.py new file mode 100644 index 0000000000..6ccce7fd4d --- /dev/null +++ b/bittensor/core/subtensor_api/staking.py @@ -0,0 +1,24 @@ +from typing import Union +from bittensor.core.subtensor import Subtensor as _Subtensor +from bittensor.core.async_subtensor import AsyncSubtensor as _AsyncSubtensor + + +class Staking: + """Class for managing staking operations.""" + + def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): + self.add_stake = subtensor.add_stake + self.add_stake_multiple = subtensor.add_stake_multiple + self.get_hotkey_stake = subtensor.get_hotkey_stake + self.get_minimum_required_stake = subtensor.get_minimum_required_stake + self.get_stake = subtensor.get_stake + self.get_stake_add_fee = subtensor.get_stake_add_fee + self.get_stake_for_coldkey = subtensor.get_stake_for_coldkey + self.get_stake_for_coldkey_and_hotkey = ( + subtensor.get_stake_for_coldkey_and_hotkey + ) + self.get_stake_info_for_coldkey = subtensor.get_stake_info_for_coldkey + self.get_stake_movement_fee = subtensor.get_stake_movement_fee + self.get_unstake_fee = subtensor.get_unstake_fee + self.unstake = subtensor.unstake + self.unstake_multiple = subtensor.unstake_multiple diff --git a/bittensor/core/subtensor_api/subnets.py b/bittensor/core/subtensor_api/subnets.py new file mode 100644 index 0000000000..c3333daf30 --- /dev/null +++ b/bittensor/core/subtensor_api/subnets.py @@ -0,0 +1,45 @@ +from typing import Union + +from bittensor.core.async_subtensor import AsyncSubtensor as _AsyncSubtensor +from bittensor.core.subtensor import Subtensor as _Subtensor + + +class Subnets: + """Class for managing subnet operations.""" + + def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): + self.all_subnets = subtensor.all_subnets + 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.difficulty = subtensor.difficulty + self.get_all_subnets_info = subtensor.get_all_subnets_info + self.get_children = subtensor.get_children + self.get_children_pending = subtensor.get_children_pending + self.get_current_weight_commit_info = subtensor.get_current_weight_commit_info + self.get_hyperparameter = subtensor.get_hyperparameter + self.get_neuron_for_pubkey_and_subnet = ( + subtensor.get_neuron_for_pubkey_and_subnet + ) + self.get_next_epoch_start_block = subtensor.get_next_epoch_start_block + self.get_subnet_burn_cost = subtensor.get_subnet_burn_cost + self.get_subnet_hyperparameters = subtensor.get_subnet_hyperparameters + self.get_subnet_owner_hotkey = subtensor.get_subnet_owner_hotkey + self.get_subnet_reveal_period_epochs = subtensor.get_subnet_reveal_period_epochs + self.get_subnet_validator_permits = subtensor.get_subnet_validator_permits + self.get_subnets = subtensor.get_subnets + self.get_total_subnets = subtensor.get_total_subnets + self.get_uid_for_hotkey_on_subnet = subtensor.get_uid_for_hotkey_on_subnet + self.immunity_period = subtensor.immunity_period + self.is_hotkey_registered_on_subnet = subtensor.is_hotkey_registered_on_subnet + self.max_weight_limit = subtensor.max_weight_limit + self.min_allowed_weights = subtensor.min_allowed_weights + self.recycle = subtensor.recycle + self.register_subnet = subtensor.register_subnet + self.set_subnet_identity = subtensor.set_subnet_identity + self.subnet = subtensor.subnet + self.subnet_exists = subtensor.subnet_exists + self.subnetwork_n = subtensor.subnetwork_n + self.tempo = subtensor.tempo + self.weights_rate_limit = subtensor.weights_rate_limit + self.weights = subtensor.weights diff --git a/bittensor/core/subtensor_api/utils.py b/bittensor/core/subtensor_api/utils.py new file mode 100644 index 0000000000..fdfde50697 --- /dev/null +++ b/bittensor/core/subtensor_api/utils.py @@ -0,0 +1,158 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from bittensor.core.subtensor_api import SubtensorApi + + +def add_legacy_methods(subtensor: "SubtensorApi"): + """If SubtensorApi get `subtensor_fields=True` arguments, then all classic Subtensor fields added to root level.""" + subtensor.add_stake = subtensor._subtensor.add_stake + subtensor.add_stake_multiple = subtensor._subtensor.add_stake_multiple + subtensor.all_subnets = subtensor._subtensor.all_subnets + subtensor.blocks_since_last_step = subtensor._subtensor.blocks_since_last_step + subtensor.blocks_since_last_update = subtensor._subtensor.blocks_since_last_update + subtensor.bonds = subtensor._subtensor.bonds + subtensor.burned_register = subtensor._subtensor.burned_register + subtensor.chain_endpoint = subtensor._subtensor.chain_endpoint + subtensor.commit = subtensor._subtensor.commit + subtensor.commit_reveal_enabled = subtensor._subtensor.commit_reveal_enabled + subtensor.commit_weights = subtensor._subtensor.commit_weights + subtensor.determine_block_hash = subtensor._subtensor.determine_block_hash + subtensor.difficulty = subtensor._subtensor.difficulty + subtensor.does_hotkey_exist = subtensor._subtensor.does_hotkey_exist + subtensor.encode_params = subtensor._subtensor.encode_params + subtensor.filter_netuids_by_registered_hotkeys = ( + subtensor._subtensor.filter_netuids_by_registered_hotkeys + ) + subtensor.get_all_commitments = subtensor._subtensor.get_all_commitments + subtensor.get_all_metagraphs_info = subtensor._subtensor.get_all_metagraphs_info + subtensor.get_all_neuron_certificates = ( + subtensor._subtensor.get_all_neuron_certificates + ) + subtensor.get_all_revealed_commitments = ( + subtensor._subtensor.get_all_revealed_commitments + ) + subtensor.get_all_subnets_info = subtensor._subtensor.get_all_subnets_info + subtensor.get_balance = subtensor._subtensor.get_balance + subtensor.get_balances = subtensor._subtensor.get_balances + subtensor.get_block_hash = subtensor._subtensor.get_block_hash + subtensor.get_children = subtensor._subtensor.get_children + subtensor.get_children_pending = subtensor._subtensor.get_children_pending + subtensor.get_commitment = subtensor._subtensor.get_commitment + subtensor.get_current_block = subtensor._subtensor.get_current_block + subtensor.get_current_weight_commit_info = ( + subtensor._subtensor.get_current_weight_commit_info + ) + 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 + subtensor.get_delegated = subtensor._subtensor.get_delegated + subtensor.get_delegates = subtensor._subtensor.get_delegates + subtensor.get_existential_deposit = subtensor._subtensor.get_existential_deposit + subtensor.get_hotkey_owner = subtensor._subtensor.get_hotkey_owner + subtensor.get_hotkey_stake = subtensor._subtensor.get_hotkey_stake + subtensor.get_hyperparameter = subtensor._subtensor.get_hyperparameter + subtensor.get_metagraph_info = subtensor._subtensor.get_metagraph_info + subtensor.get_minimum_required_stake = ( + subtensor._subtensor.get_minimum_required_stake + ) + subtensor.get_netuids_for_hotkey = subtensor._subtensor.get_netuids_for_hotkey + subtensor.get_neuron_certificate = subtensor._subtensor.get_neuron_certificate + subtensor.get_neuron_for_pubkey_and_subnet = ( + subtensor._subtensor.get_neuron_for_pubkey_and_subnet + ) + subtensor.get_next_epoch_start_block = ( + subtensor._subtensor.get_next_epoch_start_block + ) + subtensor.get_owned_hotkeys = subtensor._subtensor.get_owned_hotkeys + subtensor.get_revealed_commitment = subtensor._subtensor.get_revealed_commitment + subtensor.get_revealed_commitment_by_hotkey = ( + subtensor._subtensor.get_revealed_commitment_by_hotkey + ) + subtensor.get_stake = subtensor._subtensor.get_stake + subtensor.get_stake_add_fee = subtensor._subtensor.get_stake_add_fee + subtensor.get_stake_for_coldkey = subtensor._subtensor.get_stake_for_coldkey + subtensor.get_stake_for_coldkey_and_hotkey = ( + subtensor._subtensor.get_stake_for_coldkey_and_hotkey + ) + subtensor.get_stake_for_hotkey = subtensor._subtensor.get_stake_for_hotkey + subtensor.get_stake_info_for_coldkey = ( + subtensor._subtensor.get_stake_info_for_coldkey + ) + subtensor.get_stake_movement_fee = subtensor._subtensor.get_stake_movement_fee + subtensor.get_subnet_burn_cost = subtensor._subtensor.get_subnet_burn_cost + subtensor.get_subnet_hyperparameters = ( + subtensor._subtensor.get_subnet_hyperparameters + ) + subtensor.get_subnet_owner_hotkey = subtensor._subtensor.get_subnet_owner_hotkey + subtensor.get_subnet_reveal_period_epochs = ( + subtensor._subtensor.get_subnet_reveal_period_epochs + ) + subtensor.get_subnet_validator_permits = ( + subtensor._subtensor.get_subnet_validator_permits + ) + subtensor.get_subnets = subtensor._subtensor.get_subnets + subtensor.get_timestamp = subtensor._subtensor.get_timestamp + subtensor.get_total_subnets = subtensor._subtensor.get_total_subnets + subtensor.get_transfer_fee = subtensor._subtensor.get_transfer_fee + subtensor.get_uid_for_hotkey_on_subnet = ( + subtensor._subtensor.get_uid_for_hotkey_on_subnet + ) + subtensor.get_unstake_fee = subtensor._subtensor.get_unstake_fee + subtensor.get_vote_data = subtensor._subtensor.get_vote_data + subtensor.immunity_period = subtensor._subtensor.immunity_period + subtensor.is_fast_blocks = subtensor._subtensor.is_fast_blocks + subtensor.is_hotkey_delegate = subtensor._subtensor.is_hotkey_delegate + subtensor.is_hotkey_registered = subtensor._subtensor.is_hotkey_registered + subtensor.is_hotkey_registered_any = subtensor._subtensor.is_hotkey_registered_any + subtensor.is_hotkey_registered_on_subnet = ( + subtensor._subtensor.is_hotkey_registered_on_subnet + ) + subtensor.last_drand_round = subtensor._subtensor.last_drand_round + subtensor.log_verbose = subtensor._subtensor.log_verbose + subtensor.max_weight_limit = subtensor._subtensor.max_weight_limit + subtensor.metagraph = subtensor._subtensor.metagraph + subtensor.min_allowed_weights = subtensor._subtensor.min_allowed_weights + subtensor.move_stake = subtensor._subtensor.move_stake + subtensor.network = subtensor._subtensor.network + subtensor.neurons = subtensor._subtensor.neurons + subtensor.neuron_for_uid = subtensor._subtensor.neuron_for_uid + subtensor.neurons_lite = subtensor._subtensor.neurons_lite + subtensor.query_constant = subtensor._subtensor.query_constant + subtensor.query_identity = subtensor._subtensor.query_identity + subtensor.query_map = subtensor._subtensor.query_map + subtensor.query_map_subtensor = subtensor._subtensor.query_map_subtensor + subtensor.query_module = subtensor._subtensor.query_module + subtensor.query_runtime_api = subtensor._subtensor.query_runtime_api + subtensor.query_subtensor = subtensor._subtensor.query_subtensor + subtensor.recycle = subtensor._subtensor.recycle + subtensor.register = subtensor._subtensor.register + subtensor.register_subnet = subtensor._subtensor.register_subnet + subtensor.reveal_weights = subtensor._subtensor.reveal_weights + subtensor.root_register = subtensor._subtensor.root_register + subtensor.root_set_weights = subtensor._subtensor.root_set_weights + subtensor.serve_axon = subtensor._subtensor.serve_axon + subtensor.set_children = subtensor._subtensor.set_children + subtensor.set_commitment = subtensor._subtensor.set_commitment + subtensor.set_delegate_take = subtensor._subtensor.set_delegate_take + subtensor.set_reveal_commitment = subtensor._subtensor.set_reveal_commitment + subtensor.set_subnet_identity = subtensor._subtensor.set_subnet_identity + subtensor.set_weights = subtensor._subtensor.set_weights + subtensor.setup_config = subtensor._subtensor.setup_config + subtensor.sign_and_send_extrinsic = subtensor._subtensor.sign_and_send_extrinsic + subtensor.start_call = subtensor._subtensor.start_call + subtensor.state_call = subtensor._subtensor.state_call + subtensor.subnet = subtensor._subtensor.subnet + subtensor.subnet_exists = subtensor._subtensor.subnet_exists + subtensor.subnetwork_n = subtensor._subtensor.subnetwork_n + subtensor.substrate = subtensor._subtensor.substrate + subtensor.swap_stake = subtensor._subtensor.swap_stake + subtensor.tempo = subtensor._subtensor.tempo + subtensor.transfer = subtensor._subtensor.transfer + subtensor.transfer_stake = subtensor._subtensor.transfer_stake + subtensor.tx_rate_limit = subtensor._subtensor.tx_rate_limit + subtensor.unstake = subtensor._subtensor.unstake + subtensor.unstake_multiple = subtensor._subtensor.unstake_multiple + subtensor.wait_for_block = subtensor._subtensor.wait_for_block + subtensor.weights = subtensor._subtensor.weights + subtensor.weights_rate_limit = subtensor._subtensor.weights_rate_limit diff --git a/bittensor/core/subtensor_api/wallets.py b/bittensor/core/subtensor_api/wallets.py new file mode 100644 index 0000000000..7314a4fe39 --- /dev/null +++ b/bittensor/core/subtensor_api/wallets.py @@ -0,0 +1,39 @@ +from typing import Union +from bittensor.core.subtensor import Subtensor as _Subtensor +from bittensor.core.async_subtensor import AsyncSubtensor as _AsyncSubtensor + + +class Wallets: + """Class for managing coldkey, hotkey, wallet operations.""" + + def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): + self.does_hotkey_exist = subtensor.does_hotkey_exist + self.filter_netuids_by_registered_hotkeys = ( + subtensor.filter_netuids_by_registered_hotkeys + ) + self.is_hotkey_registered_any = subtensor.is_hotkey_registered_any + self.is_hotkey_registered = subtensor.is_hotkey_registered + self.is_hotkey_delegate = subtensor.is_hotkey_delegate + self.get_balance = subtensor.get_balance + self.get_balances = subtensor.get_balances + self.get_children = subtensor.get_children + self.get_children_pending = subtensor.get_children_pending + self.get_delegate_by_hotkey = subtensor.get_delegate_by_hotkey + self.get_delegate_take = subtensor.get_delegate_take + self.get_delegated = subtensor.get_delegated + self.get_hotkey_owner = subtensor.get_hotkey_owner + self.get_hotkey_stake = subtensor.get_hotkey_stake + self.get_minimum_required_stake = subtensor.get_minimum_required_stake + self.get_netuids_for_hotkey = subtensor.get_netuids_for_hotkey + self.get_owned_hotkeys = subtensor.get_owned_hotkeys + self.get_stake = subtensor.get_stake + self.get_stake_add_fee = subtensor.get_stake_add_fee + self.get_stake_for_coldkey = subtensor.get_stake_for_coldkey + self.get_stake_for_coldkey_and_hotkey = ( + subtensor.get_stake_for_coldkey_and_hotkey + ) + self.get_stake_for_hotkey = subtensor.get_stake_for_hotkey + self.get_stake_info_for_coldkey = subtensor.get_stake_info_for_coldkey + self.get_stake_movement_fee = subtensor.get_stake_movement_fee + self.get_transfer_fee = subtensor.get_transfer_fee + self.get_unstake_fee = subtensor.get_unstake_fee diff --git a/bittensor/core/timelock.py b/bittensor/core/timelock.py index 24a18405f4..ed0b7c892e 100644 --- a/bittensor/core/timelock.py +++ b/bittensor/core/timelock.py @@ -57,7 +57,7 @@ class Person: import time from typing import Optional, Union -from bittensor_commit_reveal import ( +from bittensor_drand import ( encrypt as _btr_encrypt, decrypt as _btr_decrypt, get_latest_round, diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index 3d2c82803b..5efe1cbb25 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -248,7 +248,7 @@ 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 = err_docs[0] if err_docs else err_description + err_description = " ".join(err_docs) elif error_message.get("code") and error_message.get("message"): err_type = error_message.get("code", err_name) diff --git a/bittensor/utils/balance.py b/bittensor/utils/balance.py index 35aa708654..d6c78d7d95 100644 --- a/bittensor/utils/balance.py +++ b/bittensor/utils/balance.py @@ -86,7 +86,7 @@ def __str__(self): """ Returns the Balance object as a string in the format "symbolvalue", where the value is in tao. """ - if self.unit == units[0]: + if self.unit == UNITS[0]: return f"{self.unit}{float(self.tao):,.9f}" else: return f"\u200e{float(self.tao):,.9f}{self.unit}\u200e" @@ -314,13 +314,13 @@ def from_rao(amount: int, netuid: int = 0) -> "Balance": @staticmethod def get_unit(netuid: int) -> str: - base = len(units) + base = len(UNITS) if netuid < base: - return units[netuid] + return UNITS[netuid] else: result = "" while netuid > 0: - result = units[netuid % base] + result + result = UNITS[netuid % base] + result netuid //= base return result @@ -360,472 +360,449 @@ def fixed_to_float( return integer_part + frac_float -units = [ - # Greek Alphabet (0-24) - "\u03c4", # Ī„ (tau, 0) - "\u03b1", # Îą (alpha, 1) - "\u03b2", # β (beta, 2) - "\u03b3", # Îŗ (gamma, 3) - "\u03b4", # δ (delta, 4) - "\u03b5", # Îĩ (epsilon, 5) - "\u03b6", # Îļ (zeta, 6) - "\u03b7", # Ρ (eta, 7) - "\u03b8", # θ (theta, 8) - "\u03b9", # Κ (iota, 9) - "\u03ba", # Îē (kappa, 10) - "\u03bb", # Îģ (lambda, 11) - "\u03bc", # Îŧ (mu, 12) - "\u03bd", # ÎŊ (nu, 13) - "\u03be", # Ξ (xi, 14) - "\u03bf", # Îŋ (omicron, 15) - "\u03c0", # Ī€ (pi, 16) - "\u03c1", # ΁ (rho, 17) - "\u03c3", # ΃ (sigma, 18) - "t", # t (tau, 19) - "\u03c5", # Ī… (upsilon, 20) - "\u03c6", # Ά (phi, 21) - "\u03c7", # · (chi, 22) - "\u03c8", # Έ (psi, 23) - "\u03c9", # Ή (omega, 24) - # Hebrew Alphabet (25-51) - "\u05d0", # א (aleph, 25) - "\u05d1", # ב (bet, 26) - "\u05d2", # ג (gimel, 27) - "\u05d3", # ד (dalet, 28) - "\u05d4", # ה (he, 29) - "\u05d5", # ו (vav, 30) - "\u05d6", # ז (zayin, 31) - "\u05d7", # ח (het, 32) - "\u05d8", # ט (tet, 33) - "\u05d9", # י (yod, 34) - "\u05da", # ך (final kaf, 35) - "\u05db", # כ (kaf, 36) - "\u05dc", # ל (lamed, 37) - "\u05dd", # ם (final mem, 38) - "\u05de", # מ (mem, 39) - "\u05df", # ן (final nun, 40) - "\u05e0", # ×  (nun, 41) - "\u05e1", # ץ (samekh, 42) - "\u05e2", # ×ĸ (ayin, 43) - "\u05e3", # ×Ŗ (final pe, 44) - "\u05e4", # פ (pe, 45) - "\u05e5", # ×Ĩ (final tsadi, 46) - "\u05e6", # ×Ļ (tsadi, 47) - "\u05e7", # ×§ (qof, 48) - "\u05e8", # ר (resh, 49) - "\u05e9", # ׊ (shin, 50) - "\u05ea", # ×Ē (tav, 51) - # Arabic Alphabet (52-81) - "\u0627", # ا (alif, 52) - "\u0628", # ب (ba, 53) - "\u062a", # ØĒ (ta, 54) - "\u062b", # ØĢ (tha, 55) - "\u062c", # ØŦ (jeem, 56) - "\u062d", # Ø­ (ha, 57) - "\u062e", # ØŽ (kha, 58) - "\u062f", # د (dal, 59) - "\u0630", # ذ (dhal, 60) - "\u0631", # Øą (ra, 61) - "\u0632", # Ø˛ (zay, 62) - "\u0633", # Øŗ (seen, 63) - "\u0634", # Ø´ (sheen, 64) - "\u0635", # Øĩ (sad, 65) - "\u0636", # Øļ (dad, 66) - "\u0637", # Øˇ (ta, 67) - "\u0638", # ظ (dha, 68) - "\u0639", # Øš (ain, 69) - "\u063a", # Øē (ghain, 70) - "\u0641", # ؁ (fa, 71) - "\u0642", # Ų‚ (qaf, 72) - "\u0643", # ؃ (kaf, 73) - "\u0644", # Ų„ (lam, 74) - "\u0645", # Ų… (meem, 75) - "\u0646", # Ų† (noon, 76) - "\u0647", # Ų‡ (ha, 77) - "\u0648", # ؈ (waw, 78) - "\u064a", # ؊ (ya, 79) - "\u0649", # Ų‰ (alef maksura, 80) - "\u064a", # ؊ (ya, 81) - # Runic Alphabet (82-90) - "\u16a0", # ᚠ (fehu, 82) - "\u16a2", # ášĸ (uruz, 83) - "\u16a6", # ášĻ (thurisaz, 84) - "\u16a8", # ᚨ (ansuz, 85) - "\u16b1", # ᚱ (raidho, 86) - "\u16b3", # ᚲ (kaunan, 87) - "\u16c7", # ᛇ (eihwaz, 88) - "\u16c9", # ᛉ (algiz, 89) - "\u16d2", # ᛒ (berkanan, 90) - # Ogham Alphabet (91-97) - "\u1680", #   (Space, 91) - "\u1681", # ᚁ (Beith, 92) - "\u1682", # ᚂ (Luis, 93) - "\u1683", # ᚃ (Fearn, 94) - "\u1684", # ᚄ (Sail, 95) - "\u1685", # ᚅ (Nion, 96) - "\u169b", # ᚛ (Forfeda, 97) - # Georgian Alphabet (98-103) - "\u10d0", # ა (ani, 98) - "\u10d1", # ბ (bani, 99) - "\u10d2", # გ (gani, 100) - "\u10d3", # დ (doni, 101) - "\u10d4", # ე (eni, 102) - "\u10d5", # ვ (vini, 103) - # Armenian Alphabet (104-110) - "\u0531", # Ôą (Ayp, 104) - "\u0532", # Ô˛ (Ben, 105) - "\u0533", # Ôŗ (Gim, 106) - "\u0534", # Ô´ (Da, 107) - "\u0535", # Ôĩ (Ech, 108) - "\u0536", # Ôļ (Za, 109) - "\u055e", # ՞ (Question mark, 110) - # Cyrillic Alphabet (111-116) - "\u0400", # Ѐ (Ie with grave, 111) - "\u0401", # Ё (Io, 112) - "\u0402", # Ђ (Dje, 113) - "\u0403", # Ѓ (Gje, 114) - "\u0404", # Є (Ukrainian Ie, 115) - "\u0405", # Ѕ (Dze, 116) - # Coptic Alphabet (117-122) - "\u2c80", # Ⲁ (Alfa, 117) - "\u2c81", # ⲁ (Small Alfa, 118) - "\u2c82", # Ⲃ (Vida, 119) - "\u2c83", # ⲃ (Small Vida, 120) - "\u2c84", # Ⲅ (Gamma, 121) - "\u2c85", # ⲅ (Small Gamma, 122) - # Brahmi Script (123-127) - "\U00011000", # 𑀀 (A, 123) - "\U00011001", # 𑀁 (Aa, 124) - "\U00011002", # 𑀂 (I, 125) - "\U00011003", # 𑀃 (Ii, 126) - "\U00011005", # 𑀅 (U, 127) - # Tifinagh Alphabet (128-133) - "\u2d30", # â´° (Ya, 128) - "\u2d31", # â´ą (Yab, 129) - "\u2d32", # â´˛ (Yabh, 130) - "\u2d33", # â´ŗ (Yag, 131) - "\u2d34", # â´´ (Yagh, 132) - "\u2d35", # â´ĩ (Yaj, 133) - # Glagolitic Alphabet (134-166) - "\u2c00", # Ⰰ (Az, 134) - "\u2c01", # Ⰱ (Buky, 135) - "\u2c02", # Ⰲ (Vede, 136) - "\u2c03", # Ⰳ (Glagoli, 137) - "\u2c04", # Ⰴ (Dobro, 138) - "\u2c05", # Ⰵ (Yest, 139) - "\u2c06", # Ⰶ (Zhivete, 140) - "\u2c07", # Ⰷ (Zemlja, 141) - "\u2c08", # Ⰸ (Izhe, 142) - "\u2c09", # Ⰹ (Initial Izhe, 143) - "\u2c0a", # Ⰺ (I, 144) - "\u2c0b", # Ⰻ (Djerv, 145) - "\u2c0c", # Ⰼ (Kako, 146) - "\u2c0d", # Ⰽ (Ljudije, 147) - "\u2c0e", # Ⰾ (Myse, 148) - "\u2c0f", # Ⰿ (Nash, 149) - "\u2c10", # Ⱀ (On, 150) - "\u2c11", # Ⱁ (Pokoj, 151) - "\u2c12", # Ⱂ (Rtsy, 152) - "\u2c13", # Ⱃ (Slovo, 153) - "\u2c14", # Ⱄ (Tvrido, 154) - "\u2c15", # Ⱅ (Uku, 155) - "\u2c16", # Ⱆ (Fert, 156) - "\u2c17", # Ⱇ (Xrivi, 157) - "\u2c18", # Ⱈ (Ot, 158) - "\u2c19", # Ⱉ (Cy, 159) - "\u2c1a", # Ⱊ (Shcha, 160) - "\u2c1b", # Ⱋ (Er, 161) - "\u2c1c", # Ⱌ (Yeru, 162) - "\u2c1d", # Ⱍ (Small Yer, 163) - "\u2c1e", # Ⱎ (Yo, 164) - "\u2c1f", # Ⱏ (Yu, 165) - "\u2c20", # â°  (Ja, 166) - # Thai Alphabet (167-210) - "\u0e01", # ⏁ (Ko Kai, 167) - "\u0e02", # ⏂ (Kho Khai, 168) - "\u0e03", # ⏃ (Kho Khuat, 169) - "\u0e04", # ⏄ (Kho Khon, 170) - "\u0e05", # ⏅ (Kho Rakhang, 171) - "\u0e06", # ⏆ (Kho Khwai, 172) - "\u0e07", # ⏇ (Ngo Ngu, 173) - "\u0e08", # ⏈ (Cho Chan, 174) - "\u0e09", # ⏉ (Cho Ching, 175) - "\u0e0a", # ⏊ (Cho Chang, 176) - "\u0e0b", # ⏋ (So So, 177) - "\u0e0c", # ⏌ (Cho Choe, 178) - "\u0e0d", # ā¸ (Yo Ying, 179) - "\u0e0e", # ā¸Ž (Do Chada, 180) - "\u0e0f", # ā¸ (To Patak, 181) - "\u0e10", # ⏐ (Tho Than, 182) - "\u0e11", # ⏑ (Tho Nangmontho, 183) - "\u0e12", # ⏒ (Tho Phuthao, 184) - "\u0e13", # ⏓ (No Nen, 185) - "\u0e14", # ⏔ (Do Dek, 186) - "\u0e15", # ⏕ (To Tao, 187) - "\u0e16", # ⏖ (Tho Thung, 188) - "\u0e17", # ⏗ (Tho Thahan, 189) - "\u0e18", # ⏘ (Tho Thong, 190) - "\u0e19", # ⏙ (No Nu, 191) - "\u0e1a", # ⏚ (Bo Baimai, 192) - "\u0e1b", # ⏛ (Po Pla, 193) - "\u0e1c", # ⏜ (Pho Phung, 194) - "\u0e1d", # ā¸ (Fo Fa, 195) - "\u0e1e", # ā¸ž (Pho Phan, 196) - "\u0e1f", # ⏟ (Fo Fan, 197) - "\u0e20", # ⏠ (Pho Samphao, 198) - "\u0e21", # ā¸Ą (Mo Ma, 199) - "\u0e22", # ā¸ĸ (Yo Yak, 200) - "\u0e23", # ⏪ (Ro Rua, 201) - "\u0e25", # ā¸Ĩ (Lo Ling, 202) - "\u0e27", # ⏧ (Wo Waen, 203) - "\u0e28", # ⏍ (So Sala, 204) - "\u0e29", # ā¸Š (So Rusi, 205) - "\u0e2a", # ā¸Ē (So Sua, 206) - "\u0e2b", # ā¸Ģ (Ho Hip, 207) - "\u0e2c", # ā¸Ŧ (Lo Chula, 208) - "\u0e2d", # ⏭ (O Ang, 209) - "\u0e2e", # ā¸Ž (Ho Nokhuk, 210) - # Hangul Consonants (211-224) - "\u1100", # ã„ą (Giyeok, 211) - "\u1101", # ㄴ (Nieun, 212) - "\u1102", # ã„ˇ (Digeut, 213) - "\u1103", # ㄚ (Rieul, 214) - "\u1104", # ㅁ (Mieum, 215) - "\u1105", # ㅂ (Bieup, 216) - "\u1106", # ㅅ (Siot, 217) - "\u1107", # ㅇ (Ieung, 218) - "\u1108", # ㅈ (Jieut, 219) - "\u1109", # ㅊ (Chieut, 220) - "\u110a", # ㅋ (Kieuk, 221) - "\u110b", # ㅌ (Tieut, 222) - "\u110c", # ㅍ (Pieup, 223) - "\u110d", # ㅎ (Hieut, 224) - # Hangul Vowels (225-245) - "\u1161", # ㅏ (A, 225) - "\u1162", # ㅐ (Ae, 226) - "\u1163", # ㅑ (Ya, 227) - "\u1164", # ㅒ (Yae, 228) - "\u1165", # ㅓ (Eo, 229) - "\u1166", # ㅔ (E, 230) - "\u1167", # ㅕ (Yeo, 231) - "\u1168", # ㅖ (Ye, 232) - "\u1169", # ㅗ (O, 233) - "\u116a", # ㅘ (Wa, 234) - "\u116b", # ㅙ (Wae, 235) - "\u116c", # ㅚ (Oe, 236) - "\u116d", # ㅛ (Yo, 237) - "\u116e", # ㅜ (U, 238) - "\u116f", # ㅝ (Weo, 239) - "\u1170", # ㅞ (We, 240) - "\u1171", # ㅟ (Wi, 241) - "\u1172", # ㅠ (Yu, 242) - "\u1173", # ã…Ą (Eu, 243) - "\u1174", # ã…ĸ (Ui, 244) - "\u1175", # ã…Ŗ (I, 245) - # Ethiopic Alphabet (246-274) - "\u12a0", # አ (Glottal A, 246) - "\u12a1", # ኡ (Glottal U, 247) - "\u12a2", # áŠĸ (Glottal I, 248) - "\u12a3", # áŠŖ (Glottal Aa, 249) - "\u12a4", # ኤ (Glottal E, 250) - "\u12a5", # áŠĨ (Glottal Ie, 251) - "\u12a6", # áŠĻ (Glottal O, 252) - "\u12a7", # ኧ (Glottal Wa, 253) - "\u12c8", # ወ (Wa, 254) - "\u12c9", # ዉ (Wu, 255) - "\u12ca", # ዊ (Wi, 256) - "\u12cb", # ዋ (Waa, 257) - "\u12cc", # ዌ (We, 258) - "\u12cd", # ው (Wye, 259) - "\u12ce", # ዎ (Wo, 260) - "\u12b0", # ኰ (Ko, 261) - "\u12b1", # ኱ (Ku, 262) - "\u12b2", # ኲ (Ki, 263) - "\u12b3", # áŠŗ (Kua, 264) - "\u12b4", # ኴ (Ke, 265) - "\u12b5", # áŠĩ (Kwe, 266) - "\u12b6", # áŠļ (Ko, 267) - "\u12a0", # ጐ (Go, 268) - "\u12a1", # ጑ (Gu, 269) - "\u12a2", # ጒ (Gi, 270) - "\u12a3", # መ (Gua, 271) - "\u12a4", # ጔ (Ge, 272) - "\u12a5", # ጕ (Gwe, 273) - "\u12a6", # ጖ (Go, 274) - # Devanagari Alphabet (275-318) - "\u0905", # ⤅ (A, 275) - "\u0906", # ⤆ (Aa, 276) - "\u0907", # ⤇ (I, 277) - "\u0908", # ⤈ (Ii, 278) - "\u0909", # ⤉ (U, 279) - "\u090a", # ⤊ (Uu, 280) - "\u090b", # ⤋ (R, 281) - "\u090f", # ā¤ (E, 282) - "\u0910", # ⤐ (Ai, 283) - "\u0913", # ⤓ (O, 284) - "\u0914", # ⤔ (Au, 285) - "\u0915", # ⤕ (Ka, 286) - "\u0916", # ⤖ (Kha, 287) - "\u0917", # ⤗ (Ga, 288) - "\u0918", # ⤘ (Gha, 289) - "\u0919", # ⤙ (Nga, 290) - "\u091a", # ⤚ (Cha, 291) - "\u091b", # ⤛ (Chha, 292) - "\u091c", # ⤜ (Ja, 293) - "\u091d", # ā¤ (Jha, 294) - "\u091e", # ā¤ž (Nya, 295) - "\u091f", # ⤟ (Ta, 296) - "\u0920", # ⤠ (Tha, 297) - "\u0921", # ā¤Ą (Da, 298) - "\u0922", # ā¤ĸ (Dha, 299) - "\u0923", # ⤪ (Na, 300) - "\u0924", # ⤤ (Ta, 301) - "\u0925", # ā¤Ĩ (Tha, 302) - "\u0926", # ā¤Ļ (Da, 303) - "\u0927", # ⤧ (Dha, 304) - "\u0928", # ⤍ (Na, 305) - "\u092a", # ā¤Ē (Pa, 306) - "\u092b", # ā¤Ģ (Pha, 307) - "\u092c", # ā¤Ŧ (Ba, 308) - "\u092d", # ⤭ (Bha, 309) - "\u092e", # ā¤Ž (Ma, 310) - "\u092f", # ⤝ (Ya, 311) - "\u0930", # ⤰ (Ra, 312) - "\u0932", # ⤞ (La, 313) - "\u0935", # ā¤ĩ (Va, 314) - "\u0936", # ā¤ļ (Sha, 315) - "\u0937", # ⤎ (Ssa, 316) - "\u0938", # ⤏ (Sa, 317) - "\u0939", # ā¤š (Ha, 318) - # Katakana Alphabet (319-364) - "\u30a2", # ã‚ĸ (A, 319) - "\u30a4", # イ (I, 320) - "\u30a6", # ã‚Ļ (U, 321) - "\u30a8", # エ (E, 322) - "\u30aa", # ã‚Ē (O, 323) - "\u30ab", # ã‚Ģ (Ka, 324) - "\u30ad", # キ (Ki, 325) - "\u30af", # ク (Ku, 326) - "\u30b1", # ã‚ą (Ke, 327) - "\u30b3", # ã‚ŗ (Ko, 328) - "\u30b5", # ã‚ĩ (Sa, 329) - "\u30b7", # ã‚ˇ (Shi, 330) - "\u30b9", # ゚ (Su, 331) - "\u30bb", # ã‚ģ (Se, 332) - "\u30bd", # ã‚Ŋ (So, 333) - "\u30bf", # ã‚ŋ (Ta, 334) - "\u30c1", # チ (Chi, 335) - "\u30c4", # ツ (Tsu, 336) - "\u30c6", # テ (Te, 337) - "\u30c8", # ト (To, 338) - "\u30ca", # ナ (Na, 339) - "\u30cb", # ニ (Ni, 340) - "\u30cc", # ヌ (Nu, 341) - "\u30cd", # ネ (Ne, 342) - "\u30ce", # ノ (No, 343) - "\u30cf", # ハ (Ha, 344) - "\u30d2", # ヒ (Hi, 345) - "\u30d5", # フ (Fu, 346) - "\u30d8", # ヘ (He, 347) - "\u30db", # ホ (Ho, 348) - "\u30de", # マ (Ma, 349) - "\u30df", # ミ (Mi, 350) - "\u30e0", # ム (Mu, 351) - "\u30e1", # ãƒĄ (Me, 352) - "\u30e2", # ãƒĸ (Mo, 353) - "\u30e4", # ヤ (Ya, 354) - "\u30e6", # ãƒĻ (Yu, 355) - "\u30e8", # ヨ (Yo, 356) - "\u30e9", # ナ (Ra, 357) - "\u30ea", # ãƒĒ (Ri, 358) - "\u30eb", # ãƒĢ (Ru, 359) - "\u30ec", # ãƒŦ (Re, 360) - "\u30ed", # ロ (Ro, 361) - "\u30ef", # ワ (Wa, 362) - "\u30f2", # ãƒ˛ (Wo, 363) - "\u30f3", # ãƒŗ (N, 364) - # Tifinagh Alphabet (365-400) - "\u2d30", # â´° (Ya, 365) - "\u2d31", # â´ą (Yab, 366) - "\u2d32", # â´˛ (Yabh, 367) - "\u2d33", # â´ŗ (Yag, 368) - "\u2d34", # â´´ (Yagh, 369) - "\u2d35", # â´ĩ (Yaj, 370) - "\u2d36", # â´ļ (Yach, 371) - "\u2d37", # â´ˇ (Yad, 372) - "\u2d38", # â´¸ (Yadh, 373) - "\u2d39", # â´š (Yadh, emphatic, 374) - "\u2d3a", # â´ē (Yaz, 375) - "\u2d3b", # â´ģ (Yazh, 376) - "\u2d3c", # â´ŧ (Yaf, 377) - "\u2d3d", # â´Ŋ (Yak, 378) - "\u2d3e", # â´ž (Yak, variant, 379) - "\u2d3f", # â´ŋ (Yaq, 380) - "\u2d40", # âĩ€ (Yah, 381) - "\u2d41", # âĩ (Yahh, 382) - "\u2d42", # âĩ‚ (Yahl, 383) - "\u2d43", # âĩƒ (Yahm, 384) - "\u2d44", # âĩ„ (Yayn, 385) - "\u2d45", # âĩ… (Yakh, 386) - "\u2d46", # âĩ† (Yakl, 387) - "\u2d47", # âĩ‡ (Yahq, 388) - "\u2d48", # âĩˆ (Yash, 389) - "\u2d49", # âĩ‰ (Yi, 390) - "\u2d4a", # âĩŠ (Yij, 391) - "\u2d4b", # âĩ‹ (Yizh, 392) - "\u2d4c", # âĩŒ (Yink, 393) - "\u2d4d", # âĩ (Yal, 394) - "\u2d4e", # âĩŽ (Yam, 395) - "\u2d4f", # âĩ (Yan, 396) - "\u2d50", # âĩ (Yang, 397) - "\u2d51", # âĩ‘ (Yany, 398) - "\u2d52", # âĩ’ (Yap, 399) - "\u2d53", # âĩ“ (Yu, 400) - # Sinhala Alphabet (401-444) - "\u0d85", # āļ… (A, 401) - "\u0d86", # āļ† (Aa, 402) - "\u0d87", # āļ‰ (I, 403) - "\u0d88", # āļŠ (Ii, 404) - "\u0d89", # āļ‹ (U, 405) - "\u0d8a", # āļŒ (Uu, 406) - "\u0d8b", # āļ (R, 407) - "\u0d8c", # āļŽ (Rr, 408) - "\u0d8f", # āļ (L, 409) - "\u0d90", # āļ (Ll, 410) - "\u0d91", # āļ‘ (E, 411) - "\u0d92", # āļ’ (Ee, 412) - "\u0d93", # āļ“ (Ai, 413) - "\u0d94", # āļ” (O, 414) - "\u0d95", # āļ• (Oo, 415) - "\u0d96", # āļ– (Au, 416) - "\u0d9a", # āļš (Ka, 417) - "\u0d9b", # āļ› (Kha, 418) - "\u0d9c", # āļœ (Ga, 419) - "\u0d9d", # āļ (Gha, 420) - "\u0d9e", # āļž (Nga, 421) - "\u0d9f", # āļ  (Cha, 422) - "\u0da0", # āļĄ (Chha, 423) - "\u0da1", # āļĸ (Ja, 424) - "\u0da2", # āļŖ (Jha, 425) - "\u0da3", # āļ¤ (Nya, 426) - "\u0da4", # āļ§ (Ta, 427) - "\u0da5", # āļĨ (Tha, 428) - "\u0da6", # āļĻ (Da, 429) - "\u0da7", # āļ§ (Dha, 430) - "\u0da8", # āļ¨ (Na, 431) - "\u0daa", # āļĒ (Pa, 432) - "\u0dab", # āļĢ (Pha, 433) - "\u0dac", # āļŦ (Ba, 434) - "\u0dad", # āļ­ (Bha, 435) - "\u0dae", # āļŽ (Ma, 436) - "\u0daf", # āļ¯ (Ya, 437) - "\u0db0", # āļ° (Ra, 438) - "\u0db1", # āļ˛ (La, 439) - "\u0db2", # āļŗ (Va, 440) - "\u0db3", # āļ´ (Sha, 441) - "\u0db4", # āļĩ (Ssa, 442) - "\u0db5", # āļļ (Sa, 443) - "\u0db6", # āļˇ (Ha, 444) +# lowercase is added for backwards compatibility to not break API +units = UNITS = [ + chr( + 0x03C4 + ), # Ī„ Note: the subnet symbol for sn 0 is b"\xce\xa4" / Τ / Tau — however the currency/balance is Ī„ (Tao) + b"\xce\xb1".decode(), # Îą (Alpha, 1) + b"\xce\xb2".decode(), # β (Beta, 2) + b"\xce\xb3".decode(), # Îŗ (Gamma, 3) + b"\xce\xb4".decode(), # δ (Delta, 4) + b"\xce\xb5".decode(), # Îĩ (Epsilon, 5) + b"\xce\xb6".decode(), # Îļ (Zeta, 6) + b"\xce\xb7".decode(), # Ρ (Eta, 7) + b"\xce\xb8".decode(), # θ (Theta, 8) + b"\xce\xb9".decode(), # Κ (Iota, 9) + b"\xce\xba".decode(), # Îē (Kappa, 10) + b"\xce\xbb".decode(), # Îģ (Lambda, 11) + b"\xce\xbc".decode(), # Îŧ (Mu, 12) + b"\xce\xbd".decode(), # ÎŊ (Nu, 13) + b"\xce\xbe".decode(), # Ξ (Xi, 14) + b"\xce\xbf".decode(), # Îŋ (Omicron, 15) + b"\xcf\x80".decode(), # Ī€ (Pi, 16) + b"\xcf\x81".decode(), # ΁ (Rho, 17) + b"\xcf\x83".decode(), # ΃ (Sigma, 18) + "t", # t (Tau, 19) + b"\xcf\x85".decode(), # Ī… (Upsilon, 20) + b"\xcf\x86".decode(), # Ά (Phi, 21) + b"\xcf\x87".decode(), # · (Chi, 22) + b"\xcf\x88".decode(), # Έ (Psi, 23) + b"\xcf\x89".decode(), # Ή (Omega, 24) + b"\xd7\x90".decode(), # א (Aleph, 25) + b"\xd7\x91".decode(), # ב (Bet, 26) + b"\xd7\x92".decode(), # ג (Gimel, 27) + b"\xd7\x93".decode(), # ד (Dalet, 28) + b"\xd7\x94".decode(), # ה (He, 29) + b"\xd7\x95".decode(), # ו (Vav, 30) + b"\xd7\x96".decode(), # ז (Zayin, 31) + b"\xd7\x97".decode(), # ח (Het, 32) + b"\xd7\x98".decode(), # ט (Tet, 33) + b"\xd7\x99".decode(), # י (Yod, 34) + b"\xd7\x9a".decode(), # ך (Final Kaf, 35) + b"\xd7\x9b".decode(), # כ (Kaf, 36) + b"\xd7\x9c".decode(), # ל (Lamed, 37) + b"\xd7\x9d".decode(), # ם (Final Mem, 38) + b"\xd7\x9e".decode(), # מ (Mem, 39) + b"\xd7\x9f".decode(), # ן (Final Nun, 40) + b"\xd7\xa0".decode(), # ×  (Nun, 41) + b"\xd7\xa1".decode(), # ץ (Samekh, 42) + b"\xd7\xa2".decode(), # ×ĸ (Ayin, 43) + b"\xd7\xa3".decode(), # ×Ŗ (Final Pe, 44) + b"\xd7\xa4".decode(), # פ (Pe, 45) + b"\xd7\xa5".decode(), # ×Ĩ (Final Tsadi, 46) + b"\xd7\xa6".decode(), # ×Ļ (Tsadi, 47) + b"\xd7\xa7".decode(), # ×§ (Qof, 48) + b"\xd7\xa8".decode(), # ר (Resh, 49) + b"\xd7\xa9".decode(), # ׊ (Shin, 50) + b"\xd7\xaa".decode(), # ×Ē (Tav, 51) + b"\xd8\xa7".decode(), # ا (Alif, 52) + b"\xd8\xa8".decode(), # ب (Ba, 53) + b"\xd8\xaa".decode(), # ØĒ (Ta, 54) + b"\xd8\xab".decode(), # ØĢ (Tha, 55) + b"\xd8\xac".decode(), # ØŦ (Jim, 56) + b"\xd8\xad".decode(), # Ø­ (Ha, 57) + b"\xd8\xae".decode(), # ØŽ (Kha, 58) + b"\xd8\xaf".decode(), # د (Dal, 59) + b"\xd8\xb0".decode(), # ذ (Dhal, 60) + b"\xd8\xb1".decode(), # Øą (Ra, 61) + b"\xd8\xb2".decode(), # Ø˛ (Zay, 62) + b"\xd8\xb3".decode(), # Øŗ (Sin, 63) + b"\xd8\xb4".decode(), # Ø´ (Shin, 64) + b"\xd8\xb5".decode(), # Øĩ (Sad, 65) + b"\xd8\xb6".decode(), # Øļ (Dad, 66) + b"\xd8\xb7".decode(), # Øˇ (Ta, 67) + b"\xd8\xb8".decode(), # ظ (Dha, 68) + b"\xd8\xb9".decode(), # Øš (Ain, 69) + b"\xd8\xba".decode(), # Øē (Ghayn, 70) + b"\xd9\x81".decode(), # ؁ (Fa, 71) + b"\xd9\x82".decode(), # Ų‚ (Qaf, 72) + b"\xd9\x83".decode(), # ؃ (Kaf, 73) + b"\xd9\x84".decode(), # Ų„ (Lam, 74) + b"\xd9\x85".decode(), # Ų… (Mim, 75) + b"\xd9\x86".decode(), # Ų† (Nun, 76) + b"\xd9\x87".decode(), # Ų‡ (Ha, 77) + b"\xd9\x88".decode(), # ؈ (Waw, 78) + b"\xd9\x8a".decode(), # ؊ (Ya, 79) + b"\xd9\x89".decode(), # Ų‰ (Alef Maksura, 80) + b"\xe1\x9a\xa0".decode(), # ᚠ (Fehu, wealth, 81) + b"\xe1\x9a\xa2".decode(), # ášĸ (Uruz, strength, 82) + b"\xe1\x9a\xa6".decode(), # ášĻ (Thurisaz, giant, 83) + b"\xe1\x9a\xa8".decode(), # ᚨ (Ansuz, god, 84) + b"\xe1\x9a\xb1".decode(), # ᚱ (Raidho, ride, 85) + b"\xe1\x9a\xb3".decode(), # ᚲ (Kaunan, ulcer, 86) + b"\xd0\xab".decode(), # ĐĢ (Cyrillic Yeru, 87) + b"\xe1\x9b\x89".decode(), # ᛉ (Algiz, protection, 88) + b"\xe1\x9b\x92".decode(), # ᛒ (Berkanan, birch, 89) + b"\xe1\x9a\x80".decode(), #   (Space, 90) + b"\xe1\x9a\x81".decode(), # ᚁ (Beith, birch, 91) + b"\xe1\x9a\x82".decode(), # ᚂ (Luis, rowan, 92) + b"\xe1\x9a\x83".decode(), # ᚃ (Fearn, alder, 93) + b"\xe1\x9a\x84".decode(), # ᚄ (Sail, willow, 94) + b"\xe1\x9a\x85".decode(), # ᚅ (Nion, ash, 95) + b"\xe1\x9a\x9b".decode(), # ᚛ (Forfeda, 96) + b"\xe1\x83\x90".decode(), # ა (Ani, 97) + b"\xe1\x83\x91".decode(), # ბ (Bani, 98) + b"\xe1\x83\x92".decode(), # გ (Gani, 99) + b"\xe1\x83\x93".decode(), # დ (Doni, 100) + b"\xe1\x83\x94".decode(), # ე (Eni, 101) + b"\xe1\x83\x95".decode(), # ვ (Vini, 102) + b"\xd4\xb1".decode(), # Ôą (Ayp, 103) + b"\xd4\xb2".decode(), # Ô˛ (Ben, 104) + b"\xd4\xb3".decode(), # Ôŗ (Gim, 105) + b"\xd4\xb4".decode(), # Ô´ (Da, 106) + b"\xd4\xb5".decode(), # Ôĩ (Ech, 107) + b"\xd4\xb6".decode(), # Ôļ (Za, 108) + b"\xd5\x9e".decode(), # ՞ (Question mark, 109) + b"\xd0\x80".decode(), # Ѐ (Ie with grave, 110) + b"\xd0\x81".decode(), # Ё (Io, 111) + b"\xd0\x82".decode(), # Ђ (Dje, 112) + b"\xd0\x83".decode(), # Ѓ (Gje, 113) + b"\xd0\x84".decode(), # Є (Ukrainian Ie, 114) + b"\xd0\x85".decode(), # Ѕ (Dze, 115) + b"\xd1\x8a".decode(), # ĐĒ (Hard sign, 116) + b"\xe2\xb2\x80".decode(), # Ⲁ (Alfa, 117) + b"\xe2\xb2\x81".decode(), # ⲁ (Small Alfa, 118) + b"\xe2\xb2\x82".decode(), # Ⲃ (Vida, 119) + b"\xe2\xb2\x83".decode(), # ⲃ (Small Vida, 120) + b"\xe2\xb2\x84".decode(), # Ⲅ (Gamma, 121) + b"\xe2\xb2\x85".decode(), # ⲅ (Small Gamma, 122) + b"\xf0\x91\x80\x80".decode(), # 𑀀 (A, 123) + b"\xf0\x91\x80\x81".decode(), # 𑀁 (Aa, 124) + b"\xf0\x91\x80\x82".decode(), # 𑀂 (I, 125) + b"\xf0\x91\x80\x83".decode(), # 𑀃 (Ii, 126) + b"\xf0\x91\x80\x85".decode(), # 𑀅 (U, 127) + b"\xe0\xb6\xb1".decode(), # āļ˛ (La, 128) + b"\xe0\xb6\xb2".decode(), # āļŗ (Va, 129) + b"\xe0\xb6\xb3".decode(), # āļ´ (Sha, 130) + b"\xe0\xb6\xb4".decode(), # āļĩ (Ssa, 131) + b"\xe0\xb6\xb5".decode(), # āļļ (Sa, 132) + b"\xe0\xb6\xb6".decode(), # āļˇ (Ha, 133) + b"\xe2\xb0\x80".decode(), # Ⰰ (Az, 134) + b"\xe2\xb0\x81".decode(), # Ⰱ (Buky, 135) + b"\xe2\xb0\x82".decode(), # Ⰲ (Vede, 136) + b"\xe2\xb0\x83".decode(), # Ⰳ (Glagoli, 137) + b"\xe2\xb0\x84".decode(), # Ⰴ (Dobro, 138) + b"\xe2\xb0\x85".decode(), # Ⰵ (Yest, 139) + b"\xe2\xb0\x86".decode(), # Ⰶ (Zhivete, 140) + b"\xe2\xb0\x87".decode(), # Ⰷ (Zemlja, 141) + b"\xe2\xb0\x88".decode(), # Ⰸ (Izhe, 142) + b"\xe2\xb0\x89".decode(), # Ⰹ (Initial Izhe, 143) + b"\xe2\xb0\x8a".decode(), # Ⰺ (I, 144) + b"\xe2\xb0\x8b".decode(), # Ⰻ (Djerv, 145) + b"\xe2\xb0\x8c".decode(), # Ⰼ (Kako, 146) + b"\xe2\xb0\x8d".decode(), # Ⰽ (Ljudije, 147) + b"\xe2\xb0\x8e".decode(), # Ⰾ (Myse, 148) + b"\xe2\xb0\x8f".decode(), # Ⰿ (Nash, 149) + b"\xe2\xb0\x90".decode(), # Ⱀ (On, 150) + b"\xe2\xb0\x91".decode(), # Ⱁ (Pokoj, 151) + b"\xe2\xb0\x92".decode(), # Ⱂ (Rtsy, 152) + b"\xe2\xb0\x93".decode(), # Ⱃ (Slovo, 153) + b"\xe2\xb0\x94".decode(), # Ⱄ (Tvrido, 154) + b"\xe2\xb0\x95".decode(), # Ⱅ (Uku, 155) + b"\xe2\xb0\x96".decode(), # Ⱆ (Fert, 156) + b"\xe2\xb0\x97".decode(), # Ⱇ (Xrivi, 157) + b"\xe2\xb0\x98".decode(), # Ⱈ (Ot, 158) + b"\xe2\xb0\x99".decode(), # Ⱉ (Cy, 159) + b"\xe2\xb0\x9a".decode(), # Ⱊ (Shcha, 160) + b"\xe2\xb0\x9b".decode(), # Ⱋ (Er, 161) + b"\xe2\xb0\x9c".decode(), # Ⱌ (Yeru, 162) + b"\xe2\xb0\x9d".decode(), # Ⱍ (Small Yer, 163) + b"\xe2\xb0\x9e".decode(), # Ⱎ (Yo, 164) + b"\xe2\xb0\x9f".decode(), # Ⱏ (Yu, 165) + b"\xe2\xb0\xa0".decode(), # â°  (Ja, 166) + b"\xe0\xb8\x81".decode(), # ⏁ (Ko Kai, 167) + b"\xe0\xb8\x82".decode(), # ⏂ (Kho Khai, 168) + b"\xe0\xb8\x83".decode(), # ⏃ (Kho Khuat, 169) + b"\xe0\xb8\x84".decode(), # ⏄ (Kho Khon, 170) + b"\xe0\xb8\x85".decode(), # ⏅ (Kho Rakhang, 171) + b"\xe0\xb8\x86".decode(), # ⏆ (Kho Khwai, 172) + b"\xe0\xb8\x87".decode(), # ⏇ (Ngo Ngu, 173) + b"\xe0\xb8\x88".decode(), # ⏈ (Cho Chan, 174) + b"\xe0\xb8\x89".decode(), # ⏉ (Cho Ching, 175) + b"\xe0\xb8\x8a".decode(), # ⏊ (Cho Chang, 176) + b"\xe0\xb8\x8b".decode(), # ⏋ (So So, 177) + b"\xe0\xb8\x8c".decode(), # ⏌ (Cho Choe, 178) + b"\xe0\xb8\x8d".decode(), # ā¸ (Yo Ying, 179) + b"\xe0\xb8\x8e".decode(), # ā¸Ž (Do Chada, 180) + b"\xe0\xb8\x8f".decode(), # ā¸ (To Patak, 181) + b"\xe0\xb8\x90".decode(), # ⏐ (Tho Than, 182) + b"\xe0\xb8\x91".decode(), # ⏑ (Tho Nangmontho, 183) + b"\xe0\xb8\x92".decode(), # ⏒ (Tho Phuthao, 184) + b"\xe0\xb8\x93".decode(), # ⏓ (No Nen, 185) + b"\xe0\xb8\x94".decode(), # ⏔ (Do Dek, 186) + b"\xe0\xb8\x95".decode(), # ⏕ (To Tao, 187) + b"\xe0\xb8\x96".decode(), # ⏖ (Tho Thung, 188) + b"\xe0\xb8\x97".decode(), # ⏗ (Tho Thahan, 189) + b"\xe0\xb8\x98".decode(), # ⏘ (Tho Thong, 190) + b"\xe0\xb8\x99".decode(), # ⏙ (No Nu, 191) + b"\xe0\xb8\x9a".decode(), # ⏚ (Bo Baimai, 192) + b"\xe0\xb8\x9b".decode(), # ⏛ (Po Pla, 193) + b"\xe0\xb8\x9c".decode(), # ⏜ (Pho Phung, 194) + b"\xe0\xb8\x9d".decode(), # ā¸ (Fo Fa, 195) + b"\xe0\xb8\x9e".decode(), # ā¸ž (Pho Phan, 196) + b"\xe0\xb8\x9f".decode(), # ⏟ (Fo Fan, 197) + b"\xe0\xb8\xa0".decode(), # ⏠ (Pho Samphao, 198) + b"\xe0\xb8\xa1".decode(), # ā¸Ą (Mo Ma, 199) + b"\xe0\xb8\xa2".decode(), # ā¸ĸ (Yo Yak, 200) + b"\xe0\xb8\xa3".decode(), # ⏪ (Ro Rua, 201) + b"\xe0\xb8\xa5".decode(), # ā¸Ĩ (Lo Ling, 202) + b"\xe0\xb8\xa7".decode(), # ⏧ (Wo Waen, 203) + b"\xe0\xb8\xa8".decode(), # ⏍ (So Sala, 204) + b"\xe0\xb8\xa9".decode(), # ā¸Š (So Rusi, 205) + b"\xe0\xb8\xaa".decode(), # ā¸Ē (So Sua, 206) + b"\xe0\xb8\xab".decode(), # ā¸Ģ (Ho Hip, 207) + b"\xe0\xb8\xac".decode(), # ā¸Ŧ (Lo Chula, 208) + b"\xe0\xb8\xad".decode(), # ⏭ (O Ang, 209) + b"\xe0\xb8\xae".decode(), # ā¸Ž (Ho Nokhuk, 210) + b"\xe1\x84\x80".decode(), # ã„ą (Giyeok, 211) + b"\xe1\x84\x81".decode(), # ㄴ (Nieun, 212) + b"\xe1\x84\x82".decode(), # ã„ˇ (Digeut, 213) + b"\xe1\x84\x83".decode(), # ㄚ (Rieul, 214) + b"\xe1\x84\x84".decode(), # ㅁ (Mieum, 215) + b"\xe1\x84\x85".decode(), # ㅂ (Bieup, 216) + b"\xe1\x84\x86".decode(), # ㅅ (Siot, 217) + b"\xe1\x84\x87".decode(), # ㅇ (Ieung, 218) + b"\xe1\x84\x88".decode(), # ㅈ (Jieut, 219) + b"\xe1\x84\x89".decode(), # ㅊ (Chieut, 220) + b"\xe1\x84\x8a".decode(), # ㅋ (Kieuk, 221) + b"\xe1\x84\x8b".decode(), # ㅌ (Tieut, 222) + b"\xe1\x84\x8c".decode(), # ㅍ (Pieup, 223) + b"\xe1\x84\x8d".decode(), # ㅎ (Hieut, 224) + b"\xe1\x85\xa1".decode(), # ㅏ (A, 225) + b"\xe1\x85\xa2".decode(), # ㅐ (Ae, 226) + b"\xe1\x85\xa3".decode(), # ㅑ (Ya, 227) + b"\xe1\x85\xa4".decode(), # ㅒ (Yae, 228) + b"\xe1\x85\xa5".decode(), # ㅓ (Eo, 229) + b"\xe1\x85\xa6".decode(), # ㅔ (E, 230) + b"\xe1\x85\xa7".decode(), # ㅕ (Yeo, 231) + b"\xe1\x85\xa8".decode(), # ㅖ (Ye, 232) + b"\xe1\x85\xa9".decode(), # ㅗ (O, 233) + b"\xe1\x85\xaa".decode(), # ㅘ (Wa, 234) + b"\xe1\x85\xab".decode(), # ㅙ (Wae, 235) + b"\xe1\x85\xac".decode(), # ㅚ (Oe, 236) + b"\xe1\x85\xad".decode(), # ㅛ (Yo, 237) + b"\xe1\x85\xae".decode(), # ㅜ (U, 238) + b"\xe1\x85\xaf".decode(), # ㅝ (Weo, 239) + b"\xe1\x85\xb0".decode(), # ㅞ (We, 240) + b"\xe1\x85\xb1".decode(), # ㅟ (Wi, 241) + b"\xe1\x85\xb2".decode(), # ㅠ (Yu, 242) + b"\xe1\x85\xb3".decode(), # ã…Ą (Eu, 243) + b"\xe1\x85\xb4".decode(), # ã…ĸ (Ui, 244) + b"\xe1\x85\xb5".decode(), # ã…Ŗ (I, 245) + b"\xe1\x8a\xa0".decode(), # አ (Glottal A, 246) + b"\xe1\x8a\xa1".decode(), # ኡ (Glottal U, 247) + b"\xe1\x8a\xa2".decode(), # áŠĸ (Glottal I, 248) + b"\xe1\x8a\xa3".decode(), # áŠŖ (Glottal Aa, 249) + b"\xe1\x8a\xa4".decode(), # ኤ (Glottal E, 250) + b"\xe1\x8a\xa5".decode(), # áŠĨ (Glottal Ie, 251) + b"\xe1\x8a\xa6".decode(), # áŠĻ (Glottal O, 252) + b"\xe1\x8a\xa7".decode(), # ኧ (Glottal Wa, 253) + b"\xe1\x8b\x88".decode(), # ወ (Wa, 254) + b"\xe1\x8b\x89".decode(), # ዉ (Wu, 255) + b"\xe1\x8b\x8a".decode(), # ዊ (Wi, 256) + b"\xe1\x8b\x8b".decode(), # ዋ (Waa, 257) + b"\xe1\x8b\x8c".decode(), # ዌ (We, 258) + b"\xe1\x8b\x8d".decode(), # ው (Wye, 259) + b"\xe1\x8b\x8e".decode(), # ዎ (Wo, 260) + b"\xe1\x8a\xb0".decode(), # ኰ (Ko, 261) + b"\xe1\x8a\xb1".decode(), # ኱ (Ku, 262) + b"\xe1\x8a\xb2".decode(), # ኲ (Ki, 263) + b"\xe1\x8a\xb3".decode(), # áŠŗ (Kua, 264) + b"\xe1\x8a\xb4".decode(), # ኴ (Ke, 265) + b"\xe1\x8a\xb5".decode(), # áŠĩ (Kwe, 266) + b"\xe1\x8a\xb6".decode(), # áŠļ (Ko, 267) + b"\xe1\x8a\x90".decode(), # ጐ (Go, 268) + b"\xe1\x8a\x91".decode(), # ጑ (Gu, 269) + b"\xe1\x8a\x92".decode(), # ጒ (Gi, 270) + b"\xe1\x8a\x93".decode(), # መ (Gua, 271) + b"\xe1\x8a\x94".decode(), # ጔ (Ge, 272) + b"\xe1\x8a\x95".decode(), # ጕ (Gwe, 273) + b"\xe1\x8a\x96".decode(), # ጖ (Go, 274) + b"\xe0\xa4\x85".decode(), # ⤅ (A, 275) + b"\xe0\xa4\x86".decode(), # ⤆ (Aa, 276) + b"\xe0\xa4\x87".decode(), # ⤇ (I, 277) + b"\xe0\xa4\x88".decode(), # ⤈ (Ii, 278) + b"\xe0\xa4\x89".decode(), # ⤉ (U, 279) + b"\xe0\xa4\x8a".decode(), # ⤊ (Uu, 280) + b"\xe0\xa4\x8b".decode(), # ⤋ (R, 281) + b"\xe0\xa4\x8f".decode(), # ā¤ (E, 282) + b"\xe0\xa4\x90".decode(), # ⤐ (Ai, 283) + b"\xe0\xa4\x93".decode(), # ⤓ (O, 284) + b"\xe0\xa4\x94".decode(), # ⤔ (Au, 285) + b"\xe0\xa4\x95".decode(), # ⤕ (Ka, 286) + b"\xe0\xa4\x96".decode(), # ⤖ (Kha, 287) + b"\xe0\xa4\x97".decode(), # ⤗ (Ga, 288) + b"\xe0\xa4\x98".decode(), # ⤘ (Gha, 289) + b"\xe0\xa4\x99".decode(), # ⤙ (Nga, 290) + b"\xe0\xa4\x9a".decode(), # ⤚ (Cha, 291) + b"\xe0\xa4\x9b".decode(), # ⤛ (Chha, 292) + b"\xe0\xa4\x9c".decode(), # ⤜ (Ja, 293) + b"\xe0\xa4\x9d".decode(), # ā¤ (Jha, 294) + b"\xe0\xa4\x9e".decode(), # ā¤ž (Nya, 295) + b"\xe0\xa4\x9f".decode(), # ⤟ (Ta, 296) + b"\xe0\xa4\xa0".decode(), # ⤠ (Tha, 297) + b"\xe0\xa4\xa1".decode(), # ā¤Ą (Da, 298) + b"\xe0\xa4\xa2".decode(), # ā¤ĸ (Dha, 299) + b"\xe0\xa4\xa3".decode(), # ⤪ (Na, 300) + b"\xe0\xa4\xa4".decode(), # ⤤ (Ta, 301) + b"\xe0\xa4\xa5".decode(), # ā¤Ĩ (Tha, 302) + b"\xe0\xa4\xa6".decode(), # ā¤Ļ (Da, 303) + b"\xe0\xa4\xa7".decode(), # ⤧ (Dha, 304) + b"\xe0\xa4\xa8".decode(), # ⤍ (Na, 305) + b"\xe0\xa4\xaa".decode(), # ā¤Ē (Pa, 306) + b"\xe0\xa4\xab".decode(), # ā¤Ģ (Pha, 307) + b"\xe0\xa4\xac".decode(), # ā¤Ŧ (Ba, 308) + b"\xe0\xa4\xad".decode(), # ⤭ (Bha, 309) + b"\xe0\xa4\xae".decode(), # ā¤Ž (Ma, 310) + b"\xe0\xa4\xaf".decode(), # ⤝ (Ya, 311) + b"\xe0\xa4\xb0".decode(), # ⤰ (Ra, 312) + b"\xe0\xa4\xb2".decode(), # ⤞ (La, 313) + b"\xe0\xa4\xb5".decode(), # ā¤ĩ (Va, 314) + b"\xe0\xa4\xb6".decode(), # ā¤ļ (Sha, 315) + b"\xe0\xa4\xb7".decode(), # ⤎ (Ssa, 316) + b"\xe0\xa4\xb8".decode(), # ⤏ (Sa, 317) + b"\xe0\xa4\xb9".decode(), # ā¤š (Ha, 318) + b"\xe3\x82\xa2".decode(), # ã‚ĸ (A, 319) + b"\xe3\x82\xa4".decode(), # イ (I, 320) + b"\xe3\x82\xa6".decode(), # ã‚Ļ (U, 321) + b"\xe3\x82\xa8".decode(), # エ (E, 322) + b"\xe3\x82\xaa".decode(), # ã‚Ē (O, 323) + b"\xe3\x82\xab".decode(), # ã‚Ģ (Ka, 324) + b"\xe3\x82\xad".decode(), # キ (Ki, 325) + b"\xe3\x82\xaf".decode(), # ク (Ku, 326) + b"\xe3\x82\xb1".decode(), # ã‚ą (Ke, 327) + b"\xe3\x82\xb3".decode(), # ã‚ŗ (Ko, 328) + b"\xe3\x82\xb5".decode(), # ã‚ĩ (Sa, 329) + b"\xe3\x82\xb7".decode(), # ã‚ˇ (Shi, 330) + b"\xe3\x82\xb9".decode(), # ゚ (Su, 331) + b"\xe3\x82\xbb".decode(), # ã‚ģ (Se, 332) + b"\xe3\x82\xbd".decode(), # ã‚Ŋ (So, 333) + b"\xe3\x82\xbf".decode(), # ã‚ŋ (Ta, 334) + b"\xe3\x83\x81".decode(), # チ (Chi, 335) + b"\xe3\x83\x84".decode(), # ツ (Tsu, 336) + b"\xe3\x83\x86".decode(), # テ (Te, 337) + b"\xe3\x83\x88".decode(), # ト (To, 338) + b"\xe3\x83\x8a".decode(), # ナ (Na, 339) + b"\xe3\x83\x8b".decode(), # ニ (Ni, 340) + b"\xe3\x83\x8c".decode(), # ヌ (Nu, 341) + b"\xe3\x83\x8d".decode(), # ネ (Ne, 342) + b"\xe3\x83\x8e".decode(), # ノ (No, 343) + b"\xe3\x83\x8f".decode(), # ハ (Ha, 344) + b"\xe3\x83\x92".decode(), # ヒ (Hi, 345) + b"\xe3\x83\x95".decode(), # フ (Fu, 346) + b"\xe3\x83\x98".decode(), # ヘ (He, 347) + b"\xe3\x83\x9b".decode(), # ホ (Ho, 348) + b"\xe3\x83\x9e".decode(), # マ (Ma, 349) + b"\xe3\x83\x9f".decode(), # ミ (Mi, 350) + b"\xe3\x83\xa0".decode(), # ム (Mu, 351) + b"\xe3\x83\xa1".decode(), # ãƒĄ (Me, 352) + b"\xe3\x83\xa2".decode(), # ãƒĸ (Mo, 353) + b"\xe3\x83\xa4".decode(), # ヤ (Ya, 354) + b"\xe3\x83\xa6".decode(), # ãƒĻ (Yu, 355) + b"\xe3\x83\xa8".decode(), # ヨ (Yo, 356) + b"\xe3\x83\xa9".decode(), # ナ (Ra, 357) + b"\xe3\x83\xaa".decode(), # ãƒĒ (Ri, 358) + b"\xe3\x83\xab".decode(), # ãƒĢ (Ru, 359) + b"\xe3\x83\xac".decode(), # ãƒŦ (Re, 360) + b"\xe3\x83\xad".decode(), # ロ (Ro, 361) + b"\xe3\x83\xaf".decode(), # ワ (Wa, 362) + b"\xe3\x83\xb2".decode(), # ãƒ˛ (Wo, 363) + b"\xe3\x83\xb3".decode(), # ãƒŗ (N, 364) + b"\xe2\xb4\xb0".decode(), # â´° (Ya, 365) + b"\xe2\xb4\xb1".decode(), # â´ą (Yab, 366) + b"\xe2\xb4\xb2".decode(), # â´˛ (Yabh, 367) + b"\xe2\xb4\xb3".decode(), # â´ŗ (Yag, 368) + b"\xe2\xb4\xb4".decode(), # â´´ (Yagh, 369) + b"\xe2\xb4\xb5".decode(), # â´ĩ (Yaj, 370) + b"\xe2\xb4\xb6".decode(), # â´ļ (Yach, 371) + b"\xe2\xb4\xb7".decode(), # â´ˇ (Yad, 372) + b"\xe2\xb4\xb8".decode(), # â´¸ (Yadh, 373) + b"\xe2\xb4\xb9".decode(), # â´š (Yadh, emphatic, 374) + b"\xe2\xb4\xba".decode(), # â´ē (Yaz, 375) + b"\xe2\xb4\xbb".decode(), # â´ģ (Yazh, 376) + b"\xe2\xb4\xbc".decode(), # â´ŧ (Yaf, 377) + b"\xe2\xb4\xbd".decode(), # â´Ŋ (Yak, 378) + b"\xe2\xb4\xbe".decode(), # â´ž (Yak, variant, 379) + b"\xe2\xb4\xbf".decode(), # â´ŋ (Yaq, 380) + b"\xe2\xb5\x80".decode(), # âĩ€ (Yah, 381) + b"\xe2\xb5\x81".decode(), # âĩ (Yahh, 382) + b"\xe2\xb5\x82".decode(), # âĩ‚ (Yahl, 383) + b"\xe2\xb5\x83".decode(), # âĩƒ (Yahm, 384) + b"\xe2\xb5\x84".decode(), # âĩ„ (Yayn, 385) + b"\xe2\xb5\x85".decode(), # âĩ… (Yakh, 386) + b"\xe2\xb5\x86".decode(), # âĩ† (Yakl, 387) + b"\xe2\xb5\x87".decode(), # âĩ‡ (Yahq, 388) + b"\xe2\xb5\x88".decode(), # âĩˆ (Yash, 389) + b"\xe2\xb5\x89".decode(), # âĩ‰ (Yi, 390) + b"\xe2\xb5\x8a".decode(), # âĩŠ (Yij, 391) + b"\xe2\xb5\x8b".decode(), # âĩ‹ (Yizh, 392) + b"\xe2\xb5\x8c".decode(), # âĩŒ (Yink, 393) + b"\xe2\xb5\x8d".decode(), # âĩ (Yal, 394) + b"\xe2\xb5\x8e".decode(), # âĩŽ (Yam, 395) + b"\xe2\xb5\x8f".decode(), # âĩ (Yan, 396) + b"\xe2\xb5\x90".decode(), # âĩ (Yang, 397) + b"\xe2\xb5\x91".decode(), # âĩ‘ (Yany, 398) + b"\xe2\xb5\x92".decode(), # âĩ’ (Yap, 399) + b"\xe2\xb5\x93".decode(), # âĩ“ (Yu, 400) + b"\xe0\xb6\x85".decode(), # āļ… (A, 401) + b"\xe0\xb6\x86".decode(), # āļ† (Aa, 402) + b"\xe0\xb6\x87".decode(), # āļ‰ (I, 403) + b"\xe0\xb6\x88".decode(), # āļŠ (Ii, 404) + b"\xe0\xb6\x89".decode(), # āļ‹ (U, 405) + b"\xe0\xb6\x8a".decode(), # āļŒ (Uu, 406) + b"\xe0\xb6\x8b".decode(), # āļ (R, 407) + b"\xe0\xb6\x8c".decode(), # āļŽ (Rr, 408) + b"\xe0\xb6\x8f".decode(), # āļ (L, 409) + b"\xe0\xb6\x90".decode(), # āļ (Ll, 410) + b"\xe0\xb6\x91".decode(), # āļ‘ (E, 411) + b"\xe0\xb6\x92".decode(), # āļ’ (Ee, 412) + b"\xe0\xb6\x93".decode(), # āļ“ (Ai, 413) + b"\xe0\xb6\x94".decode(), # āļ” (O, 414) + b"\xe0\xb6\x95".decode(), # āļ• (Oo, 415) + b"\xe0\xb6\x96".decode(), # āļ– (Au, 416) + b"\xe0\xb6\x9a".decode(), # āļš (Ka, 417) + b"\xe0\xb6\x9b".decode(), # āļ› (Kha, 418) + b"\xe0\xb6\x9c".decode(), # āļœ (Ga, 419) + b"\xe0\xb6\x9d".decode(), # āļ (Gha, 420) + b"\xe0\xb6\x9e".decode(), # āļž (Nga, 421) + b"\xe0\xb6\x9f".decode(), # āļ  (Cha, 422) + b"\xe0\xb6\xa0".decode(), # āļĄ (Chha, 423) + b"\xe0\xb6\xa1".decode(), # āļĸ (Ja, 424) + b"\xe0\xb6\xa2".decode(), # āļŖ (Jha, 425) + b"\xe0\xb6\xa3".decode(), # āļ¤ (Nya, 426) + b"\xe0\xb6\xa4".decode(), # āļ§ (Ta, 427) + b"\xe0\xb6\xa5".decode(), # āļĨ (Tha, 428) + b"\xe0\xb6\xa6".decode(), # āļĻ (Da, 429) + b"\xe0\xb6\xa7".decode(), # āļ§ (Dha, 430) + b"\xe0\xb6\xa8".decode(), # āļ¨ (Na, 431) + b"\xe0\xb6\xaa".decode(), # āļĒ (Pa, 432) + b"\xe0\xb6\xab".decode(), # āļĢ (Pha, 433) + b"\xe0\xb6\xac".decode(), # āļŦ (Ba, 434) + b"\xe0\xb6\xad".decode(), # āļ­ (Bha, 435) + b"\xe0\xb6\xae".decode(), # āļŽ (Ma, 436) + b"\xe0\xb6\xaf".decode(), # āļ¯ (Ya, 437) + b"\xe0\xb6\xb0".decode(), # āļ° (Ra, 438) ] diff --git a/bittensor/utils/easy_imports.py b/bittensor/utils/easy_imports.py index cc81efe3d2..a2645b0668 100644 --- a/bittensor/utils/easy_imports.py +++ b/bittensor/utils/easy_imports.py @@ -7,9 +7,9 @@ import importlib import sys -from bittensor_wallet import Keypair # noqa: F401 -from bittensor_wallet.errors import KeyFileError # noqa: F401 -from bittensor_wallet.keyfile import ( # noqa: F401 +from bittensor_wallet import Keypair +from bittensor_wallet.errors import KeyFileError +from bittensor_wallet.keyfile import ( serialized_keypair_to_keyfile_data, deserialize_keypair_from_keyfile_data, validate_password, @@ -25,12 +25,12 @@ decrypt_keyfile_data, Keyfile, ) -from bittensor_wallet.wallet import display_mnemonic_msg, Wallet # noqa: F401 +from bittensor_wallet.wallet import display_mnemonic_msg, Wallet -from bittensor.core import settings, timelock # noqa: F401 +from bittensor.core import settings, timelock from bittensor.core.async_subtensor import AsyncSubtensor from bittensor.core.axon import Axon -from bittensor.core.chain_data import ( # noqa: F401 +from bittensor.core.chain_data import ( AxonInfo, ChainIdentity, DelegateInfo, @@ -47,6 +47,7 @@ ProposalCallData, ProposalVoteData, ScheduledColdkeySwapInfo, + SelectiveMetagraphIndex, StakeInfo, SubnetHyperparameters, SubnetIdentity, @@ -54,14 +55,9 @@ SubnetState, WeightCommitInfo, ) -from bittensor.core.config import ( # noqa: F401 - InvalidConfigFile, - DefaultConfig, - Config, - T, -) -from bittensor.core.dendrite import Dendrite # noqa: F401 -from bittensor.core.errors import ( # noqa: F401 +from bittensor.core.config import Config +from bittensor.core.dendrite import Dendrite +from bittensor.core.errors import ( BlacklistedException, ChainConnectionError, ChainError, @@ -101,14 +97,13 @@ ) from bittensor.core.metagraph import Metagraph from bittensor.core.settings import BLOCKTIME -from bittensor.core.stream import StreamingSynapse # noqa: F401 +from bittensor.core.stream import StreamingSynapse from bittensor.core.subtensor import Subtensor -from bittensor.core.synapse import TerminalInfo, Synapse # noqa: F401 -from bittensor.core.tensor import Tensor # noqa: F401 -from bittensor.core.threadpool import ( # noqa: F401 - PriorityThreadPoolExecutor as PriorityThreadPoolExecutor, -) -from bittensor.utils import ( # noqa: F401 +from bittensor.core.subtensor_api import SubtensorApi +from bittensor.core.synapse import TerminalInfo, Synapse +from bittensor.core.tensor import Tensor +from bittensor.core.threadpool import PriorityThreadPoolExecutor +from bittensor.utils import ( ss58_to_vec_u8, version_checking, strtobool, @@ -118,14 +113,12 @@ u64_normalized_float, get_hash, ) -from bittensor.utils.balance import Balance as Balance # noqa: F401 +from bittensor.utils.balance import Balance from bittensor.utils.balance import tao, rao from bittensor.utils.btlogging import logging -from bittensor.utils.mock.subtensor_mock import MockSubtensor as MockSubtensor # noqa: F401 -from bittensor.utils.subnets import SubnetsAPI # noqa: F401 +from bittensor.utils.mock.subtensor_mock import MockSubtensor +from bittensor.utils.subnets import SubnetsAPI -tao = tao -rao = rao # Backwards compatibility with previous bittensor versions. async_subtensor = AsyncSubtensor @@ -138,23 +131,6 @@ subtensor = Subtensor synapse = Synapse -__blocktime__ = BLOCKTIME -__network_explorer_map__ = settings.NETWORK_EXPLORER_MAP -__pipaddress__ = settings.PIPADDRESS -__ss58_format__ = settings.SS58_FORMAT -__type_registry__ = settings.TYPE_REGISTRY -__ss58_address_length__ = settings.SS58_ADDRESS_LENGTH - -__networks__ = settings.NETWORKS - -__finney_entrypoint__ = settings.FINNEY_ENTRYPOINT -__finney_test_entrypoint__ = settings.FINNEY_TEST_ENTRYPOINT -__archive_entrypoint__ = settings.ARCHIVE_ENTRYPOINT -__local_entrypoint__ = settings.LOCAL_ENTRYPOINT - -__tao_symbol__ = settings.TAO_SYMBOL -__rao_symbol__ = settings.RAO_SYMBOL - # Makes the `bittensor.utils.mock` subpackage available as `bittensor.mock` for backwards compatibility. mock_subpackage = importlib.import_module("bittensor.utils.mock") sys.modules["bittensor.mock"] = mock_subpackage @@ -199,3 +175,128 @@ def info(on: bool = True): on (bool): If True, enables info logging. If False, disables info logging and sets default (WARNING) level. """ logging.set_info(on) + + +__all__ = [ + "Keypair", + "KeyFileError", + "serialized_keypair_to_keyfile_data", + "deserialize_keypair_from_keyfile_data", + "validate_password", + "ask_password_to_encrypt", + "keyfile_data_is_encrypted_nacl", + "keyfile_data_is_encrypted_ansible", + "keyfile_data_is_encrypted_legacy", + "keyfile_data_is_encrypted", + "keyfile_data_encryption_method", + "legacy_encrypt_keyfile_data", + "encrypt_keyfile_data", + "get_coldkey_password_from_environment", + "decrypt_keyfile_data", + "Keyfile", + "display_mnemonic_msg", + "Wallet", + "settings", + "timelock", + "AsyncSubtensor", + "Axon", + "AxonInfo", + "ChainIdentity", + "DelegateInfo", + "DelegateInfoLite", + "DynamicInfo", + "IPInfo", + "MetagraphInfo", + "MetagraphInfoEmissions", + "MetagraphInfoParams", + "MetagraphInfoPool", + "NeuronInfo", + "NeuronInfoLite", + "PrometheusInfo", + "ProposalCallData", + "ProposalVoteData", + "ScheduledColdkeySwapInfo", + "SelectiveMetagraphIndex", + "StakeInfo", + "SubnetHyperparameters", + "SubnetIdentity", + "SubnetInfo", + "SubnetState", + "WeightCommitInfo", + "Config", + "Dendrite", + "BlacklistedException", + "ChainConnectionError", + "ChainError", + "ChainQueryError", + "ChainTransactionError", + "DelegateTakeTooHigh", + "DelegateTakeTooLow", + "DelegateTxRateLimitExceeded", + "DuplicateChild", + "HotKeyAccountNotExists", + "IdentityError", + "InternalServerError", + "InvalidChild", + "InvalidRequestNameError", + "MetadataError", + "NominationError", + "NonAssociatedColdKey", + "NotDelegateError", + "NotEnoughStakeToSetChildkeys", + "NotRegisteredError", + "NotVerifiedException", + "PostProcessException", + "PriorityException", + "ProportionOverflow", + "RegistrationError", + "RegistrationNotPermittedOnRootSubnet", + "RunException", + "StakeError", + "SubNetworkDoesNotExist", + "SynapseDendriteNoneException", + "SynapseParsingError", + "TooManyChildren", + "TransferError", + "TxRateLimitExceeded", + "UnknownSynapseError", + "UnstakeError", + "Metagraph", + "BLOCKTIME", + "StreamingSynapse", + "Subtensor", + "SubtensorApi", + "TerminalInfo", + "Synapse", + "Tensor", + "PriorityThreadPoolExecutor", + "ss58_to_vec_u8", + "version_checking", + "strtobool", + "get_explorer_url_for_network", + "ss58_address_to_bytes", + "u16_normalized_float", + "u64_normalized_float", + "get_hash", + "Balance", + "tao", + "rao", + "logging", + "MockSubtensor", + "SubnetsAPI", + "async_subtensor", + "axon", + "config", + "dendrite", + "keyfile", + "metagraph", + "wallet", + "subtensor", + "synapse", + "trace", + "debug", + "warning", + "info", + "mock_subpackage", + "extrinsics_subpackage", +] diff --git a/bittensor/utils/mock/subtensor_mock.py b/bittensor/utils/mock/subtensor_mock.py index e2aeec8758..9773d525a4 100644 --- a/bittensor/utils/mock/subtensor_mock.py +++ b/bittensor/utils/mock/subtensor_mock.py @@ -1,6 +1,7 @@ from collections.abc import Mapping from dataclasses import dataclass from hashlib import sha256 +from random import randint from types import SimpleNamespace from typing import Any, Optional, Union, TypedDict from unittest.mock import MagicMock, patch @@ -358,6 +359,104 @@ def set_difficulty(self, netuid: int, difficulty: int) -> None: subtensor_state["Difficulty"][netuid][self.block_number] = difficulty + def _register_neuron(self, netuid: int, hotkey: str, coldkey: str) -> int: + subtensor_state = self.chain_state["SubtensorModule"] + if netuid not in subtensor_state["NetworksAdded"]: + raise Exception("Subnet does not exist") + + subnetwork_n = self._get_most_recent_storage( + subtensor_state["SubnetworkN"][netuid] + ) + + if subnetwork_n > 0 and any( + self._get_most_recent_storage(subtensor_state["Keys"][netuid][uid]) + == hotkey + for uid in range(subnetwork_n) + ): + # already_registered + raise Exception("Hotkey already registered") + else: + # Not found + if subnetwork_n >= self._get_most_recent_storage( + subtensor_state["MaxAllowedUids"][netuid] + ): + # Subnet full, replace neuron randomly + uid = randint(0, subnetwork_n - 1) + else: + # Subnet not full, add new neuron + # Append as next uid and increment subnetwork_n + uid = subnetwork_n + subtensor_state["SubnetworkN"][netuid][self.block_number] = ( + subnetwork_n + 1 + ) + + subtensor_state["Stake"][hotkey] = {} + subtensor_state["Stake"][hotkey][coldkey] = {} + subtensor_state["Stake"][hotkey][coldkey][self.block_number] = 0 + + subtensor_state["Uids"][netuid][hotkey] = {} + subtensor_state["Uids"][netuid][hotkey][self.block_number] = uid + + subtensor_state["Keys"][netuid][uid] = {} + subtensor_state["Keys"][netuid][uid][self.block_number] = hotkey + + subtensor_state["Owner"][hotkey] = {} + subtensor_state["Owner"][hotkey][self.block_number] = coldkey + + subtensor_state["Active"][netuid][uid] = {} + subtensor_state["Active"][netuid][uid][self.block_number] = True + + subtensor_state["LastUpdate"][netuid][uid] = {} + subtensor_state["LastUpdate"][netuid][uid][self.block_number] = ( + self.block_number + ) + + subtensor_state["Rank"][netuid][uid] = {} + subtensor_state["Rank"][netuid][uid][self.block_number] = 0.0 + + subtensor_state["Emission"][netuid][uid] = {} + subtensor_state["Emission"][netuid][uid][self.block_number] = 0.0 + + subtensor_state["Incentive"][netuid][uid] = {} + subtensor_state["Incentive"][netuid][uid][self.block_number] = 0.0 + + subtensor_state["Consensus"][netuid][uid] = {} + subtensor_state["Consensus"][netuid][uid][self.block_number] = 0.0 + + subtensor_state["Trust"][netuid][uid] = {} + subtensor_state["Trust"][netuid][uid][self.block_number] = 0.0 + + subtensor_state["ValidatorTrust"][netuid][uid] = {} + subtensor_state["ValidatorTrust"][netuid][uid][self.block_number] = 0.0 + + subtensor_state["Dividends"][netuid][uid] = {} + subtensor_state["Dividends"][netuid][uid][self.block_number] = 0.0 + + subtensor_state["PruningScores"][netuid][uid] = {} + subtensor_state["PruningScores"][netuid][uid][self.block_number] = 0.0 + + subtensor_state["ValidatorPermit"][netuid][uid] = {} + subtensor_state["ValidatorPermit"][netuid][uid][self.block_number] = False + + subtensor_state["Weights"][netuid][uid] = {} + subtensor_state["Weights"][netuid][uid][self.block_number] = [] + + subtensor_state["Bonds"][netuid][uid] = {} + subtensor_state["Bonds"][netuid][uid][self.block_number] = [] + + subtensor_state["Axons"][netuid][hotkey] = {} + subtensor_state["Axons"][netuid][hotkey][self.block_number] = {} + + subtensor_state["Prometheus"][netuid][hotkey] = {} + subtensor_state["Prometheus"][netuid][hotkey][self.block_number] = {} + + if hotkey not in subtensor_state["IsNetworkMember"]: + subtensor_state["IsNetworkMember"][hotkey] = {} + subtensor_state["IsNetworkMember"][hotkey][netuid] = {} + subtensor_state["IsNetworkMember"][hotkey][netuid][self.block_number] = True + + return uid + @staticmethod def _convert_to_balance(balance: Union["Balance", float, int]) -> "Balance": if isinstance(balance, float): @@ -368,6 +467,37 @@ def _convert_to_balance(balance: Union["Balance", float, int]) -> "Balance": return balance + def force_register_neuron( + self, + netuid: int, + hotkey: str, + coldkey: str, + stake: Union["Balance", float, int] = Balance(0), + balance: Union["Balance", float, int] = Balance(0), + ) -> int: + """ + Force register a neuron on the mock chain, returning the UID. + """ + stake = self._convert_to_balance(stake) + balance = self._convert_to_balance(balance) + + subtensor_state = self.chain_state["SubtensorModule"] + if netuid not in subtensor_state["NetworksAdded"]: + raise Exception("Subnet does not exist") + + uid = self._register_neuron(netuid=netuid, hotkey=hotkey, coldkey=coldkey) + + subtensor_state["TotalStake"][self.block_number] = ( + self._get_most_recent_storage(subtensor_state["TotalStake"]) + stake.rao + ) + subtensor_state["Stake"][hotkey][coldkey][self.block_number] = stake.rao + + if balance.rao > 0: + self.force_set_balance(coldkey, balance) + self.force_set_balance(coldkey, balance) + + return uid + def force_set_balance( self, ss58_address: str, balance: Union["Balance", float, int] = Balance(0) ) -> tuple[bool, Optional[str]]: diff --git a/bittensor/utils/weight_utils.py b/bittensor/utils/weight_utils.py index 5c98b1f383..2a7b7f3afd 100644 --- a/bittensor/utils/weight_utils.py +++ b/bittensor/utils/weight_utils.py @@ -440,3 +440,42 @@ def generate_weight_hash( commit_hash = "0x" + blake2b_hash.hexdigest() return commit_hash + + +def convert_uids_and_weights( + uids: Union[NDArray[np.int64], list], + weights: Union[NDArray[np.float32], list], +) -> tuple[np.ndarray, np.ndarray]: + """Converts netuids and weights to numpy arrays if they are not already. + + Arguments: + uids (Union[NDArray[np.int64], list]): The uint64 uids of destination neurons. + weights (Union[NDArray[np.float32], list]): The weights to set. These must be floated. + + Returns: + tuple[ndarray, ndarray]: Bytes converted netuids and weights. + """ + if isinstance(uids, list): + uids = np.array(uids, dtype=np.int64) + if isinstance(weights, list): + weights = np.array(weights, dtype=np.float32) + return uids, weights + + +def convert_and_normalize_weights_and_uids( + uids: Union[NDArray[np.int64], "torch.LongTensor", list], + weights: Union[NDArray[np.float32], "torch.FloatTensor", list], +) -> tuple[list[int], list[int]]: + """Converts weights and uids to numpy arrays if they are not already. + + Arguments: + uids (Union[NDArray[np.int64], torch.LongTensor, list]): The ``uint64`` uids of destination neurons. + weights (Union[NDArray[np.float32], torch.FloatTensor, list]): The weights to set. These must be ``float`` s + and correspond to the passed ``uid`` s. + + Returns: + weight_uids, weight_vals: Bytes converted weights and uids + """ + + # Reformat and normalize and return + return convert_weights_and_uids_for_emit(*convert_uids_and_weights(uids, weights)) diff --git a/contrib/CONTRIBUTING.md b/contrib/CONTRIBUTING.md index e0a0a75cee..714102bfd2 100644 --- a/contrib/CONTRIBUTING.md +++ b/contrib/CONTRIBUTING.md @@ -102,7 +102,7 @@ Patchsets and enhancements should always be focused. A pull request could add a Specifically, pull requests must adhere to the following criteria: - **Must** branch off from `staging`. Make sure that all your PRs are using `staging` branch as a base or will be closed. - Contain fewer than 50 files. PRs with more than 50 files will be closed. -- Use the specific [template](./.github/pull_request_template.md) appropriate to your contribution. +- Use the specific [template](../.github/pull_request_template.md) appropriate to your contribution. - If a PR introduces a new feature, it *must* include corresponding tests. - Other PRs (bug fixes, refactoring, etc.) should ideally also have tests, as they provide proof of concept and prevent regression. - Categorize your PR properly by using GitHub labels. This aids in the review process by informing reviewers about the type of change at a glance. @@ -127,7 +127,7 @@ Please follow these steps to have your contribution considered by the maintainer 1. Read the [development workflow](./DEVELOPMENT_WORKFLOW.md) defined for this repository to understand our workflow. 2. Ensure your PR meets the criteria stated in the 'Pull Request Philosophy' section. 3. Include relevant tests for any fixed bugs or new features as stated in the [testing guide](./TESTING.md). -4. Follow all instructions in [the template](https://github.com/opentensor/bittensor/blob/master/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md) to create the PR. +4. Follow all instructions in [the template](../.github/pull_request_template.md) to create the PR. 5. Ensure your commit messages are clear and concise. Include the issue number if applicable. 6. If you have multiple commits, rebase them into a single commit using `git rebase -i`. 7. Explain what your changes do and why you think they should be merged in the PR description consistent with the [style guide](./STYLE.md). diff --git a/pyproject.toml b/pyproject.toml index 18d80f6c44..a9b9db6a01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bittensor" -version = "9.4.0" +version = "9.5.0" description = "Bittensor" readme = "README.md" authors = [ @@ -34,9 +34,9 @@ dependencies = [ "pydantic>=2.3, <3", "scalecodec==1.2.11", "uvicorn", - "bittensor-commit-reveal>=0.4.0", + "bittensor-drand>=0.5.0", "bittensor-wallet>=3.0.8", - "async-substrate-interface>=1.1.0" + "async-substrate-interface>=1.2.0" ] [project.optional-dependencies] diff --git a/scripts/check_requirements_changes.sh b/scripts/check_requirements_changes.sh index 5b41f463c1..4a736f525d 100755 --- a/scripts/check_requirements_changes.sh +++ b/scripts/check_requirements_changes.sh @@ -1,7 +1,7 @@ #!/bin/bash # Check if requirements files have changed in the last commit -if git diff --name-only HEAD~1 | grep -E 'pyproject.toml'; then +if git diff --name-only HEAD | grep -E 'pyproject.toml'; then echo "Requirements files may have changed. Running compatibility checks..." echo 'export REQUIREMENTS_CHANGED="true"' >> $BASH_ENV else diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index 0170cbd302..ab1e840102 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -11,8 +11,7 @@ import pytest from async_substrate_interface import SubstrateInterface -from bittensor.core.async_subtensor import AsyncSubtensor -from bittensor.core.subtensor import Subtensor +from bittensor.core.subtensor_api import SubtensorApi from bittensor.utils.btlogging import logging from tests.e2e_tests.utils.e2e_test_utils import ( Templates, @@ -57,7 +56,11 @@ def read_output(): def local_chain(request): """Determines whether to run the localnet.sh script in a subprocess or a Docker container.""" args = request.param if hasattr(request, "param") else None - params = "" if args is None else f"{args}" + + # passed env variable to control node mod (non-/fast-blocks) + fast_blocks = "False" if (os.getenv("FAST_BLOCKS") == "0") is True else "True" + params = f"{fast_blocks}" if args is None else f"{fast_blocks} {args} " + if shutil.which("docker") and not os.getenv("USE_DOCKER") == "0": yield from docker_runner(params) else: @@ -91,7 +94,7 @@ def legacy_runner(params): logging.warning("LOCALNET_SH_PATH env variable is not set, e2e test skipped.") pytest.skip("LOCALNET_SH_PATH environment variable is not set.") - # Check if param is None, and handle it accordingly + # Check if param is None and handle it accordingly args = "" if params is None else f"{params}" # Compile commands to send to process @@ -202,10 +205,13 @@ def stop_existing_test_containers(): "9944:9944", "-p", "9945:9945", - LOCALNET_IMAGE_NAME, - params, + str(LOCALNET_IMAGE_NAME), ] + cmds += params.split() if params else [] + + print("Entire run command: ", cmds) + try_start_docker() stop_existing_test_containers() @@ -251,12 +257,14 @@ def templates(): @pytest.fixture def subtensor(local_chain): - return Subtensor(network="ws://localhost:9944") + return SubtensorApi(network="ws://localhost:9944", legacy_methods=True) @pytest.fixture def async_subtensor(local_chain): - return AsyncSubtensor(network="ws://localhost:9944") + return SubtensorApi( + network="ws://localhost:9944", legacy_methods=True, async_subtensor=True + ) @pytest.fixture @@ -271,7 +279,19 @@ def bob_wallet(): return wallet +@pytest.fixture +def charlie_wallet(): + keypair, wallet = setup_wallet("//Charlie") + return wallet + + @pytest.fixture def dave_wallet(): keypair, wallet = setup_wallet("//Dave") return wallet + + +@pytest.fixture +def eve_wallet(): + keypair, wallet = setup_wallet("//Eve") + return wallet diff --git a/tests/e2e_tests/test_axon.py b/tests/e2e_tests/test_axon.py index 0a139eb457..613027c691 100644 --- a/tests/e2e_tests/test_axon.py +++ b/tests/e2e_tests/test_axon.py @@ -13,7 +13,7 @@ async def test_axon(subtensor, templates, alice_wallet): Steps: 1. Register a subnet and register Alice 2. Check if metagraph.axon is updated and check axon attributes - 3. Run Alice as a miner on the subnet + 3. Run Alice as a miner on subnet 4. Check the metagraph again after running the miner and verify all attributes Raises: AssertionError: If any of the checks or verifications fail diff --git a/tests/e2e_tests/test_commit_reveal_v3.py b/tests/e2e_tests/test_commit_reveal_v3.py index 106af50a96..719dbe5827 100644 --- a/tests/e2e_tests/test_commit_reveal_v3.py +++ b/tests/e2e_tests/test_commit_reveal_v3.py @@ -22,7 +22,7 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle Steps: 1. Register a subnet through Alice 2. Register Alice's neuron and add stake - 3. Enable commit-reveal mechanism on the subnet + 3. Enable a commit-reveal mechanism on subnet 4. Lower weights rate limit 5. Change the tempo for subnet 1 5. Commit weights and ensure they are committed. @@ -30,17 +30,22 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle Raises: AssertionError: If any of the checks or verifications fail """ - BLOCK_TIME = 0.25 # 12 for non-fast-block, 0.25 for fast block - netuid = 2 + 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") # 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), "Subnet wasn't created successfully" + assert subtensor.subnet_exists(netuid), ( + f"Subnet {netuid} wasn't created successfully" + ) - logging.console.info("Subnet 2 is registered") + logging.console.success(f"Subnet {netuid} is registered") # Enable commit_reveal on the subnet assert sudo_set_hyperparameter_bool( @@ -74,7 +79,7 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle logging.console.info("sudo_set_weights_set_rate_limit executed: set to 0") # Change the tempo of the subnet - tempo_set = 50 + tempo_set = 50 if subtensor.is_fast_blocks() else 10 assert ( sudo_set_admin_utils( local_chain, @@ -103,7 +108,7 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle ) # Wait for 2 tempos to pass as CR3 only reveals weights after 2 tempos + 1 - subtensor.wait_for_block((tempo_set * 2) + 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: @@ -117,7 +122,7 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle latest_drand_round = subtensor.last_drand_round() upcoming_tempo = next_tempo(current_block, tempo) logging.console.info( - f"Post first wait_interval (to ensure window isnt too low): {current_block}, next tempo: {upcoming_tempo}, drand: {latest_drand_round}" + f"Post first wait_interval (to ensure window isn't too low): {current_block}, next tempo: {upcoming_tempo}, drand: {latest_drand_round}" ) # Commit weights @@ -129,6 +134,7 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle wait_for_inclusion=True, wait_for_finalization=True, block_time=BLOCK_TIME, + period=16, ) # Assert committing was a success @@ -170,6 +176,7 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle subtensor, netuid=netuid, reporting_interval=1, + sleep=BLOCK_TIME, ) # Fetch the latest drand pulse diff --git a/tests/e2e_tests/test_commit_weights.py b/tests/e2e_tests/test_commit_weights.py index 1b39dd411e..7f98d8a6f6 100644 --- a/tests/e2e_tests/test_commit_weights.py +++ b/tests/e2e_tests/test_commit_weights.py @@ -19,15 +19,15 @@ async def test_commit_and_reveal_weights_legacy(local_chain, subtensor, alice_wa Steps: 1. Register a subnet through Alice - 2. Enable commit-reveal mechanism on the subnet + 2. Enable the commit-reveal mechanism on subnet 3. Lower the commit_reveal interval and rate limit 4. Commit weights and verify 5. Wait interval & reveal weights and verify Raises: AssertionError: If any of the checks or verifications fail """ - netuid = 2 - + netuid = subtensor.get_total_subnets() # 2 + set_tempo = 100 if subtensor.is_fast_blocks() else 10 print("Testing test_commit_and_reveal_weights") # Register root as Alice @@ -78,7 +78,7 @@ async def test_commit_and_reveal_weights_legacy(local_chain, subtensor, alice_wa call_function="sudo_set_tempo", call_params={ "netuid": netuid, - "tempo": 100, + "tempo": set_tempo, }, ) @@ -153,23 +153,23 @@ async def test_commit_and_reveal_weights_legacy(local_chain, subtensor, alice_wa @pytest.mark.asyncio async def test_commit_weights_uses_next_nonce(local_chain, subtensor, alice_wallet): """ - Tests that commiting weights doesn't re-use a nonce in the transaction pool. + Tests that committing weights doesn't re-use nonce in the transaction pool. Steps: 1. Register a subnet through Alice 2. Register Alice's neuron and add stake - 3. Enable commit-reveal mechanism on the subnet + 3. Enable the commit-reveal mechanism on subnet 4. Lower the commit_reveal interval and rate limit 5. Commit weights three times 6. Assert that all commits succeeded Raises: AssertionError: If any of the checks or verifications fail """ - subnet_tempo = 50 - netuid = 2 + subnet_tempo = 50 if subtensor.is_fast_blocks() else 10 + netuid = subtensor.get_total_subnets() # 2 # Wait for 2 tempos to pass as CR3 only reveals weights after 2 tempos - subtensor.wait_for_block(subnet_tempo * 2 + 1) + subtensor.wait_for_block(subtensor.block + (subnet_tempo * 2) + 1) print("Testing test_commit_and_reveal_weights") # Register root as Alice @@ -226,7 +226,7 @@ async def test_commit_weights_uses_next_nonce(local_chain, subtensor, alice_wall # wait while weights_rate_limit changes applied. subtensor.wait_for_block(subnet_tempo + 1) - # create different commited data to avoid coming into pool black list with the error + # Create different commited data to avoid coming into the pool's blacklist with the error # Failed to commit weights: Subtensor returned `Custom type(1012)` error. This means: `Transaction is temporarily # banned`.Failed to commit weights: Subtensor returned `Custom type(1012)` error. This means: `Transaction is # temporarily banned`.` @@ -245,7 +245,7 @@ def get_weights_and_salt(counter: int): f"{subtensor.substrate.get_account_next_index(alice_wallet.hotkey.ss58_address)}[/orange]" ) - # 3 time doing call if nonce wasn't updated, then raise error + # 3 time doing call if nonce wasn't updated, then raise the error @retry.retry(exceptions=Exception, tries=3, delay=1) @execute_and_wait_for_next_nonce(subtensor=subtensor, wallet=alice_wallet) def send_commit(salt_, weight_uids_, weight_vals_): @@ -260,15 +260,16 @@ def send_commit(salt_, weight_uids_, weight_vals_): ) assert success is True, message - # send some amount of commit weights + # Send some number of commit weights AMOUNT_OF_COMMIT_WEIGHTS = 3 for call in range(AMOUNT_OF_COMMIT_WEIGHTS): weight_uids, weight_vals, salt = get_weights_and_salt(call) send_commit(salt, weight_uids, weight_vals) - # let's wait for 3 (12 fast blocks) seconds between transactions - subtensor.wait_for_block(subtensor.block + 12) + # let's wait for 3 (12 fast blocks) seconds between transactions, next block for non-fast-blocks + waiting_block = (subtensor.block + 12) if subtensor.is_fast_blocks() else None + subtensor.wait_for_block(waiting_block) logging.console.info( f"[orange]Nonce after third commit_weights: " @@ -276,7 +277,12 @@ def send_commit(salt_, weight_uids_, weight_vals_): ) # Wait a few blocks - subtensor.wait_for_block(subtensor.block + subtensor.tempo(netuid) * 2) + waiting_block = ( + (subtensor.block + subtensor.tempo(netuid) * 2) + if subtensor.is_fast_blocks() + else None + ) + subtensor.wait_for_block(waiting_block) # Query the WeightCommits storage map for all three salts weight_commits = subtensor.query_module( diff --git a/tests/e2e_tests/test_commitment.py b/tests/e2e_tests/test_commitment.py index e4704c8d8f..a6a33f98c2 100644 --- a/tests/e2e_tests/test_commitment.py +++ b/tests/e2e_tests/test_commitment.py @@ -3,7 +3,10 @@ from bittensor import logging from tests.e2e_tests.utils.chain_interactions import sudo_set_admin_utils -from tests.e2e_tests.utils.e2e_test_utils import wait_to_start_call +from tests.e2e_tests.utils.e2e_test_utils import ( + wait_to_start_call, + async_wait_to_start_call, +) logging.set_trace() @@ -15,7 +18,7 @@ def test_commitment(local_chain, subtensor, alice_wallet, dave_wallet): "Subnet wasn't created successfully" ) - assert wait_to_start_call(subtensor, dave_wallet, dave_subnet_netuid, 10) + assert wait_to_start_call(subtensor, dave_wallet, dave_subnet_netuid) with pytest.raises(SubstrateRequestException, match="AccountNotAllowedCommit"): subtensor.set_commitment( @@ -93,11 +96,9 @@ async def test_commitment_async( "Subnet wasn't created successfully" ) - await async_subtensor.wait_for_block(await async_subtensor.block + 20) - status, message = await async_subtensor.start_call( - dave_wallet, dave_subnet_netuid, True, True + assert await async_wait_to_start_call( + async_subtensor, dave_wallet, dave_subnet_netuid ) - assert status, message async with async_subtensor as sub: with pytest.raises(SubstrateRequestException, match="AccountNotAllowedCommit"): diff --git a/tests/e2e_tests/test_delegate.py b/tests/e2e_tests/test_delegate.py index c66691351d..c3157c9db1 100644 --- a/tests/e2e_tests/test_delegate.py +++ b/tests/e2e_tests/test_delegate.py @@ -171,7 +171,7 @@ def test_change_take(local_chain, subtensor, alice_wallet, bob_wallet): @pytest.mark.asyncio -async def test_delegates(subtensor, alice_wallet, bob_wallet): +async def test_delegates(local_chain, subtensor, alice_wallet, bob_wallet): """ Tests: - Check default Delegates @@ -240,6 +240,7 @@ async def test_delegates(subtensor, alice_wallet, bob_wallet): assert subtensor.get_delegated(bob_wallet.coldkey.ss58_address) == [] alice_subnet_netuid = subtensor.get_total_subnets() # 2 + set_tempo = 10 # Register a subnet, netuid 2 assert subtensor.register_subnet(alice_wallet), "Subnet wasn't created" @@ -250,6 +251,17 @@ async def test_delegates(subtensor, alice_wallet, bob_wallet): assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) + # set the same tempo for both type of nodes (fast and non-fast blocks) + assert ( + sudo_set_admin_utils( + local_chain, + alice_wallet, + call_function="sudo_set_tempo", + call_params={"netuid": alice_subnet_netuid, "tempo": set_tempo}, + )[0] + is True + ) + subtensor.add_stake( bob_wallet, alice_wallet.hotkey.ss58_address, @@ -259,6 +271,9 @@ async def test_delegates(subtensor, alice_wallet, bob_wallet): wait_for_finalization=True, ) + # let chain update validator_permits + subtensor.wait_for_block(subtensor.block + set_tempo + 1) + bob_delegated = subtensor.get_delegated(bob_wallet.coldkey.ss58_address) assert bob_delegated == [ DelegatedInfo( @@ -272,9 +287,10 @@ async def test_delegates(subtensor, alice_wallet, bob_wallet): bob_delegated[0].total_daily_return.rao ), netuid=alice_subnet_netuid, - stake=get_dynamic_balance(bob_delegated[0].stake.rao), + stake=get_dynamic_balance(bob_delegated[0].stake.rao, alice_subnet_netuid), ), ] + bittensor.logging.console.success("Test [green]test_delegates[/green] passed.") def test_nominator_min_required_stake(local_chain, subtensor, alice_wallet, bob_wallet): diff --git a/tests/e2e_tests/test_hotkeys.py b/tests/e2e_tests/test_hotkeys.py index f28f4c07f9..3a699f08d0 100644 --- a/tests/e2e_tests/test_hotkeys.py +++ b/tests/e2e_tests/test_hotkeys.py @@ -1,14 +1,22 @@ import pytest -import bittensor -from tests.e2e_tests.utils.chain_interactions import ( - sudo_set_admin_utils, - wait_epoch, +from bittensor.core.errors import ( + NotEnoughStakeToSetChildkeys, + RegistrationNotPermittedOnRootSubnet, + SubNetworkDoesNotExist, + InvalidChild, + TooManyChildren, + ProportionOverflow, + DuplicateChild, + TxRateLimitExceeded, + NonAssociatedColdKey, ) +from bittensor.utils.btlogging import logging +from tests.e2e_tests.utils.chain_interactions import sudo_set_admin_utils from tests.e2e_tests.utils.e2e_test_utils import wait_to_start_call -SET_CHILDREN_COOLDOWN_PERIOD = 15 -SET_CHILDREN_RATE_LIMIT = 150 + +SET_CHILDREN_RATE_LIMIT = 15 def test_hotkeys(subtensor, alice_wallet, dave_wallet): @@ -17,8 +25,7 @@ def test_hotkeys(subtensor, alice_wallet, dave_wallet): - Check if Hotkey exists - Check if Hotkey is registered """ - - dave_subnet_netuid = 2 + dave_subnet_netuid = subtensor.get_total_subnets() # 2 assert subtensor.register_subnet(dave_wallet, True, True) assert subtensor.subnet_exists(dave_subnet_netuid), ( f"Subnet #{dave_subnet_netuid} does not exist." @@ -62,6 +69,7 @@ def test_hotkeys(subtensor, alice_wallet, dave_wallet): ) is True ) + logging.console.success(f"✅ Test [green]test_hotkeys[/green] passed") @pytest.mark.asyncio @@ -76,7 +84,9 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet, dave_w - Clear children list """ - dave_subnet_netuid = 2 + dave_subnet_netuid = subtensor.get_total_subnets() # 2 + set_tempo = 10 # affect to non-fast-blocks mode + assert subtensor.register_subnet(dave_wallet, True, True) assert subtensor.subnet_exists(dave_subnet_netuid), ( f"Subnet #{dave_subnet_netuid} does not exist." @@ -84,7 +94,29 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet, dave_w assert wait_to_start_call(subtensor, dave_wallet, dave_subnet_netuid) - with pytest.raises(bittensor.RegistrationNotPermittedOnRootSubnet): + # set the same tempo for both type of nodes (to avoid tests timeout) + if not subtensor.is_fast_blocks(): + assert ( + sudo_set_admin_utils( + local_chain, + alice_wallet, + call_function="sudo_set_tempo", + call_params={"netuid": dave_subnet_netuid, "tempo": set_tempo}, + )[0] + is True + ) + + assert ( + sudo_set_admin_utils( + local_chain, + alice_wallet, + call_function="sudo_set_tx_rate_limit", + call_params={"tx_rate_limit": 0}, + )[0] + is True + ) + + with pytest.raises(RegistrationNotPermittedOnRootSubnet): subtensor.set_children( alice_wallet, alice_wallet.hotkey.ss58_address, @@ -93,7 +125,7 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet, dave_w raise_error=True, ) - with pytest.raises(bittensor.NonAssociatedColdKey): + with pytest.raises(NonAssociatedColdKey): subtensor.set_children( alice_wallet, alice_wallet.hotkey.ss58_address, @@ -102,7 +134,7 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet, dave_w raise_error=True, ) - with pytest.raises(bittensor.SubNetworkDoesNotExist): + with pytest.raises(SubNetworkDoesNotExist): subtensor.set_children( alice_wallet, alice_wallet.hotkey.ss58_address, @@ -115,10 +147,12 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet, dave_w alice_wallet, netuid=dave_subnet_netuid, ) + logging.console.success(f"Alice registered on subnet {dave_subnet_netuid}") subtensor.burned_register( bob_wallet, netuid=dave_subnet_netuid, ) + logging.console.success(f"Bob registered on subnet {dave_subnet_netuid}") success, children, error = subtensor.get_children( alice_wallet.hotkey.ss58_address, @@ -129,7 +163,7 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet, dave_w assert success is True assert children == [] - with pytest.raises(bittensor.InvalidChild): + with pytest.raises(InvalidChild): subtensor.set_children( alice_wallet, alice_wallet.hotkey.ss58_address, @@ -143,7 +177,7 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet, dave_w raise_error=True, ) - with pytest.raises(bittensor.TooManyChildren): + with pytest.raises(TooManyChildren): subtensor.set_children( alice_wallet, alice_wallet.hotkey.ss58_address, @@ -158,7 +192,7 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet, dave_w raise_error=True, ) - with pytest.raises(bittensor.ProportionOverflow): + with pytest.raises(ProportionOverflow): subtensor.set_children( alice_wallet, alice_wallet.hotkey.ss58_address, @@ -176,7 +210,7 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet, dave_w raise_error=True, ) - with pytest.raises(bittensor.DuplicateChild): + with pytest.raises(DuplicateChild): subtensor.set_children( alice_wallet, alice_wallet.hotkey.ss58_address, @@ -229,16 +263,9 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet, dave_w netuid=dave_subnet_netuid, ) - assert pending == [ - ( - 1.0, - "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty", - ), - ] + assert pending == [(1.0, bob_wallet.hotkey.ss58_address)] - subtensor.wait_for_block(cooldown) - - await wait_epoch(subtensor, netuid=1) + subtensor.wait_for_block(cooldown + 15) success, children, error = subtensor.get_children( alice_wallet.hotkey.ss58_address, @@ -247,22 +274,24 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet, dave_w assert error == "" assert success is True - assert children == [ - ( - 1.0, - bob_wallet.hotkey.ss58_address, - ) - ] + assert children == [(1.0, bob_wallet.hotkey.ss58_address)] # pending queue is empty pending, cooldown = subtensor.get_children_pending( alice_wallet.hotkey.ss58_address, netuid=dave_subnet_netuid, ) - assert pending == [] - with pytest.raises(bittensor.TxRateLimitExceeded): + with pytest.raises(TxRateLimitExceeded): + set_children_block = subtensor.get_current_block() + subtensor.set_children( + alice_wallet, + alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + children=[], + raise_error=True, + ) subtensor.set_children( alice_wallet, alice_wallet.hotkey.ss58_address, @@ -311,7 +340,7 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet, dave_w }, ) - with pytest.raises(bittensor.NotEnoughStakeToSetChildkeys): + with pytest.raises(NotEnoughStakeToSetChildkeys): subtensor.set_children( alice_wallet, alice_wallet.hotkey.ss58_address, @@ -324,3 +353,5 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet, dave_w ], raise_error=True, ) + + logging.console.success(f"✅ Test [green]test_children[/green] passed") diff --git a/tests/e2e_tests/test_metagraph.py b/tests/e2e_tests/test_metagraph.py index 3338a0959e..c3c485ebbe 100644 --- a/tests/e2e_tests/test_metagraph.py +++ b/tests/e2e_tests/test_metagraph.py @@ -4,6 +4,7 @@ import time from bittensor.core.chain_data.metagraph_info import MetagraphInfo +from bittensor.core.chain_data import SelectiveMetagraphIndex from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging from tests.e2e_tests.utils.e2e_test_utils import wait_to_start_call @@ -206,22 +207,22 @@ def test_metagraph_info(subtensor, alice_wallet, bob_wallet): symbol="Îą", identity=None, network_registered_at=0, - owner_hotkey=(NULL_KEY,), - owner_coldkey=(NULL_KEY,), + owner_hotkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + owner_coldkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", block=1, tempo=100, last_step=0, blocks_since_last_step=1, subnet_emission=Balance(0), - alpha_in=Balance.from_tao(10), - alpha_out=Balance.from_tao(1), + alpha_in=Balance.from_tao(10).set_unit(netuid=alice_subnet_netuid), + alpha_out=Balance.from_tao(1).set_unit(netuid=alice_subnet_netuid), tao_in=Balance.from_tao(10), - alpha_out_emission=Balance(0), - alpha_in_emission=Balance(0), + alpha_out_emission=Balance(0).set_unit(netuid=alice_subnet_netuid), + alpha_in_emission=Balance(0).set_unit(netuid=alice_subnet_netuid), tao_in_emission=Balance(0), - pending_alpha_emission=Balance(0), + pending_alpha_emission=Balance(0).set_unit(netuid=alice_subnet_netuid), pending_root_emission=Balance(0), - subnet_volume=Balance(0), + subnet_volume=Balance(0).set_unit(netuid=alice_subnet_netuid), moving_price=Balance(0), rho=10, kappa=32767, @@ -272,16 +273,16 @@ def test_metagraph_info(subtensor, alice_wallet, bob_wallet): validator_permit=(False,), pruning_score=[0.0], last_update=(0,), - emission=[Balance(0)], + emission=[Balance(0).set_unit(alice_subnet_netuid)], dividends=[0.0], incentives=[0.0], consensus=[0.0], trust=[0.0], rank=[0.0], block_at_registration=(0,), - alpha_stake=[Balance.from_tao(1.0)], + alpha_stake=[Balance.from_tao(1.0).set_unit(alice_subnet_netuid)], tao_stake=[Balance(0)], - total_stake=[Balance.from_tao(1.0)], + total_stake=[Balance.from_tao(1.0).set_unit(alice_subnet_netuid)], tao_dividends_per_hotkey=[ ("5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", Balance(0)) ], @@ -299,18 +300,18 @@ def test_metagraph_info(subtensor, alice_wallet, bob_wallet): symbol="Τ", identity=None, network_registered_at=0, - owner_hotkey=(NULL_KEY,), - owner_coldkey=(NULL_KEY,), + owner_hotkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + owner_coldkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", block=1, tempo=100, last_step=0, blocks_since_last_step=1, subnet_emission=Balance(0), - alpha_in=Balance(0), - alpha_out=Balance(0), + alpha_in=Balance(0).set_unit(netuid=alice_subnet_netuid), + alpha_out=Balance(0).set_unit(netuid=alice_subnet_netuid), tao_in=Balance(0), - alpha_out_emission=Balance(0), - alpha_in_emission=Balance(0), + alpha_out_emission=Balance(0).set_unit(netuid=alice_subnet_netuid), + alpha_in_emission=Balance(0).set_unit(netuid=alice_subnet_netuid), tao_in_emission=Balance(0), pending_alpha_emission=Balance(0), pending_root_emission=Balance(0), @@ -420,8 +421,8 @@ def test_metagraph_info(subtensor, alice_wallet, bob_wallet): netuid=alice_subnet_netuid, block=block ) - assert metagraph_info.owner_coldkey == (tuple(alice_wallet.hotkey.public_key),) - assert metagraph_info.owner_hotkey == (tuple(alice_wallet.coldkey.public_key),) + assert metagraph_info.owner_coldkey == alice_wallet.hotkey.ss58_address + assert metagraph_info.owner_hotkey == alice_wallet.coldkey.ss58_address metagraph_infos = subtensor.get_all_metagraphs_info(block) @@ -434,6 +435,235 @@ def test_metagraph_info(subtensor, alice_wallet, bob_wallet): assert metagraph_info is None +# TODO: get back after SelectiveMetagraph come to the mainnet +# def test_metagraph_info_with_indexes(subtensor, alice_wallet, bob_wallet): +# """ +# Tests: +# - Check MetagraphInfo +# - Register Neuron +# - Register Subnet +# - Check MetagraphInfo is updated +# """ +# +# alice_subnet_netuid = subtensor.get_total_subnets() # 2 +# subtensor.register_subnet(alice_wallet, True, True) +# +# field_indices = [ +# SelectiveMetagraphIndex.Name, +# SelectiveMetagraphIndex.Active, +# SelectiveMetagraphIndex.OwnerHotkey, +# SelectiveMetagraphIndex.OwnerColdkey, +# SelectiveMetagraphIndex.Axons, +# ] +# +# metagraph_info = subtensor.get_metagraph_info( +# netuid=alice_subnet_netuid, field_indices=field_indices +# ) +# +# assert metagraph_info == MetagraphInfo( +# netuid=alice_subnet_netuid, +# name="omron", +# owner_hotkey=alice_wallet.hotkey.ss58_address, +# owner_coldkey=alice_wallet.coldkey.ss58_address, +# active=(True,), +# axons=( +# { +# "block": 0, +# "ip": 0, +# "ip_type": 0, +# "placeholder1": 0, +# "placeholder2": 0, +# "port": 0, +# "protocol": 0, +# "version": 0, +# }, +# ), +# symbol=None, +# identity=None, +# network_registered_at=None, +# block=None, +# tempo=None, +# last_step=None, +# blocks_since_last_step=None, +# subnet_emission=None, +# alpha_in=None, +# alpha_out=None, +# tao_in=None, +# alpha_out_emission=None, +# alpha_in_emission=None, +# tao_in_emission=None, +# pending_alpha_emission=None, +# pending_root_emission=None, +# subnet_volume=None, +# moving_price=None, +# rho=None, +# kappa=None, +# min_allowed_weights=None, +# max_weights_limit=None, +# weights_version=None, +# weights_rate_limit=None, +# activity_cutoff=None, +# max_validators=None, +# num_uids=None, +# max_uids=None, +# burn=None, +# difficulty=None, +# registration_allowed=None, +# pow_registration_allowed=None, +# immunity_period=None, +# min_difficulty=None, +# max_difficulty=None, +# min_burn=None, +# max_burn=None, +# adjustment_alpha=None, +# adjustment_interval=None, +# target_regs_per_interval=None, +# max_regs_per_block=None, +# serving_rate_limit=None, +# commit_reveal_weights_enabled=None, +# commit_reveal_period=None, +# liquid_alpha_enabled=None, +# alpha_high=None, +# alpha_low=None, +# bonds_moving_avg=None, +# hotkeys=None, +# coldkeys=None, +# identities=None, +# validator_permit=None, +# pruning_score=None, +# last_update=None, +# emission=None, +# dividends=None, +# incentives=None, +# consensus=None, +# trust=None, +# rank=None, +# block_at_registration=None, +# alpha_stake=None, +# tao_stake=None, +# total_stake=None, +# tao_dividends_per_hotkey=None, +# alpha_dividends_per_hotkey=None, +# ) +# +# assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) +# +# assert subtensor.burned_register( +# bob_wallet, +# netuid=alice_subnet_netuid, +# wait_for_inclusion=True, +# wait_for_finalization=True, +# ) +# +# fields = [ +# SelectiveMetagraphIndex.Name, +# SelectiveMetagraphIndex.Active, +# SelectiveMetagraphIndex.OwnerHotkey, +# SelectiveMetagraphIndex.OwnerColdkey, +# SelectiveMetagraphIndex.Axons, +# ] +# +# metagraph_info = subtensor.get_metagraph_info( +# netuid=alice_subnet_netuid, field_indices=fields +# ) +# +# assert metagraph_info == MetagraphInfo( +# netuid=alice_subnet_netuid, +# name="omron", +# owner_hotkey=alice_wallet.hotkey.ss58_address, +# owner_coldkey=alice_wallet.coldkey.ss58_address, +# active=(True, True), +# axons=( +# { +# "block": 0, +# "ip": 0, +# "ip_type": 0, +# "placeholder1": 0, +# "placeholder2": 0, +# "port": 0, +# "protocol": 0, +# "version": 0, +# }, +# { +# "block": 0, +# "ip": 0, +# "ip_type": 0, +# "placeholder1": 0, +# "placeholder2": 0, +# "port": 0, +# "protocol": 0, +# "version": 0, +# }, +# ), +# symbol=None, +# identity=None, +# network_registered_at=None, +# block=None, +# tempo=None, +# last_step=None, +# blocks_since_last_step=None, +# subnet_emission=None, +# alpha_in=None, +# alpha_out=None, +# tao_in=None, +# alpha_out_emission=None, +# alpha_in_emission=None, +# tao_in_emission=None, +# pending_alpha_emission=None, +# pending_root_emission=None, +# subnet_volume=None, +# moving_price=None, +# rho=None, +# kappa=None, +# min_allowed_weights=None, +# max_weights_limit=None, +# weights_version=None, +# weights_rate_limit=None, +# activity_cutoff=None, +# max_validators=None, +# num_uids=None, +# max_uids=None, +# burn=None, +# difficulty=None, +# registration_allowed=None, +# pow_registration_allowed=None, +# immunity_period=None, +# min_difficulty=None, +# max_difficulty=None, +# min_burn=None, +# max_burn=None, +# adjustment_alpha=None, +# adjustment_interval=None, +# target_regs_per_interval=None, +# max_regs_per_block=None, +# serving_rate_limit=None, +# commit_reveal_weights_enabled=None, +# commit_reveal_period=None, +# liquid_alpha_enabled=None, +# alpha_high=None, +# alpha_low=None, +# bonds_moving_avg=None, +# hotkeys=None, +# coldkeys=None, +# identities=None, +# validator_permit=None, +# pruning_score=None, +# last_update=None, +# emission=None, +# dividends=None, +# incentives=None, +# consensus=None, +# trust=None, +# rank=None, +# block_at_registration=None, +# alpha_stake=None, +# tao_stake=None, +# total_stake=None, +# tao_dividends_per_hotkey=None, +# alpha_dividends_per_hotkey=None, +# ) + + def test_blocks(subtensor): """ Tests: diff --git a/tests/e2e_tests/test_reveal_commitments.py b/tests/e2e_tests/test_reveal_commitments.py index 40cc8794a1..c4757e24cb 100644 --- a/tests/e2e_tests/test_reveal_commitments.py +++ b/tests/e2e_tests/test_reveal_commitments.py @@ -26,8 +26,10 @@ async def test_set_reveal_commitment(local_chain, subtensor, alice_wallet, bob_w Note: Actually we can run this tests in fast block mode. For this we need to set `BLOCK_TIME` to 0.25 and replace `False` to `True` in `pytest.mark.parametrize` decorator. """ - BLOCK_TIME = 0.25 # 12 for non-fast-block, 0.25 for fast block - BLOCKS_UNTIL_REVEAL = 10 + BLOCK_TIME = ( + 0.25 if subtensor.is_fast_blocks() else 12.0 + ) # 12 for non-fast-block, 0.25 for fast block + BLOCKS_UNTIL_REVEAL = 10 if subtensor.is_fast_blocks() else 5 alice_subnet_netuid = subtensor.get_total_subnets() # 2 @@ -79,7 +81,8 @@ async def test_set_reveal_commitment(local_chain, subtensor, alice_wallet, bob_w # Sometimes the chain doesn't update the repository right away and the commit doesn't appear in the expected # `last_drand_round`. In this case need to wait a bit. print(f"Waiting for reveal round {target_reveal_round}") - while subtensor.last_drand_round() <= target_reveal_round + 1: + chain_offset = 1 if subtensor.is_fast_blocks() else 24 + while subtensor.last_drand_round() <= target_reveal_round + chain_offset: # wait one drand period (3 sec) print(f"Current last reveled drand round {subtensor.last_drand_round()}") time.sleep(3) diff --git a/tests/e2e_tests/test_root_set_weights.py b/tests/e2e_tests/test_root_set_weights.py index f399a071b3..5973a144cb 100644 --- a/tests/e2e_tests/test_root_set_weights.py +++ b/tests/e2e_tests/test_root_set_weights.py @@ -62,7 +62,7 @@ async def test_root_reg_hyperparams( # Default immunity period & tempo set through the subtensor side default_immunity_period = 5000 - default_tempo = 10 + default_tempo = 10 if subtensor.is_fast_blocks() else 360 # 0.2 for root network, 0.8 for sn 1 # Corresponding to [0.2, 0.8] diff --git a/tests/e2e_tests/test_set_subnet_identity_extrinsic.py b/tests/e2e_tests/test_set_subnet_identity_extrinsic.py index 60d91fa3d1..11c8d76d7e 100644 --- a/tests/e2e_tests/test_set_subnet_identity_extrinsic.py +++ b/tests/e2e_tests/test_set_subnet_identity_extrinsic.py @@ -10,7 +10,7 @@ async def test_set_subnet_identity_extrinsic_happy_pass(subtensor, alice_wallet) "[magenta]Testing `set_subnet_identity_extrinsic` with success result.[/magenta]" ) - netuid = 2 + netuid = subtensor.get_total_subnets() # 2 # Register a subnet, netuid 2 assert subtensor.register_subnet(alice_wallet), "Subnet wasn't created" diff --git a/tests/e2e_tests/test_staking.py b/tests/e2e_tests/test_staking.py index 55afa58beb..f9bcc45510 100644 --- a/tests/e2e_tests/test_staking.py +++ b/tests/e2e_tests/test_staking.py @@ -5,8 +5,6 @@ from tests.helpers.helpers import ApproxBalance from tests.e2e_tests.utils.e2e_test_utils import wait_to_start_call -logging.enable_info() - def test_single_operation(subtensor, alice_wallet, bob_wallet): """ @@ -28,56 +26,61 @@ def test_single_operation(subtensor, alice_wallet, bob_wallet): assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) subtensor.burned_register( - alice_wallet, + wallet=alice_wallet, netuid=alice_subnet_netuid, wait_for_inclusion=True, wait_for_finalization=True, ) + logging.console.success(f"Alice is registered in subnet {alice_subnet_netuid}") subtensor.burned_register( - bob_wallet, + wallet=bob_wallet, netuid=alice_subnet_netuid, wait_for_inclusion=True, wait_for_finalization=True, ) + logging.console.success(f"Bob is registered in subnet {alice_subnet_netuid}") stake = subtensor.get_stake( - alice_wallet.coldkey.ss58_address, - bob_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, netuid=alice_subnet_netuid, ) assert stake == Balance(0).set_unit(alice_subnet_netuid) success = subtensor.add_stake( - alice_wallet, - bob_wallet.hotkey.ss58_address, + wallet=alice_wallet, + hotkey_ss58=bob_wallet.hotkey.ss58_address, netuid=alice_subnet_netuid, amount=Balance.from_tao(1), wait_for_inclusion=True, wait_for_finalization=True, + period=16, ) assert success is True stake_alice = subtensor.get_stake( - alice_wallet.coldkey.ss58_address, - alice_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=alice_wallet.hotkey.ss58_address, netuid=alice_subnet_netuid, ) + logging.console.info(f"Alice stake: {stake_alice}") stake_bob = subtensor.get_stake( - alice_wallet.coldkey.ss58_address, - bob_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, netuid=alice_subnet_netuid, ) + logging.console.info(f"Bob stake: {stake_bob}") assert stake_bob > Balance(0).set_unit(alice_subnet_netuid) stakes = subtensor.get_stake_for_coldkey(alice_wallet.coldkey.ss58_address) - assert stakes == [ + expected_stakes = [ StakeInfo( - hotkey_ss58=alice_wallet.hotkey.ss58_address, + hotkey_ss58=stakes[0].hotkey_ss58, coldkey_ss58=alice_wallet.coldkey.ss58_address, netuid=alice_subnet_netuid, stake=get_dynamic_balance(stakes[0].stake.rao, alice_subnet_netuid), @@ -86,42 +89,31 @@ def test_single_operation(subtensor, alice_wallet, bob_wallet): drain=0, is_registered=True, ), - StakeInfo( - hotkey_ss58=bob_wallet.hotkey.ss58_address, - coldkey_ss58=alice_wallet.coldkey.ss58_address, - netuid=alice_subnet_netuid, - stake=get_dynamic_balance(stakes[1].stake.rao, alice_subnet_netuid), - locked=Balance(0).set_unit(alice_subnet_netuid), - emission=get_dynamic_balance(stakes[1].emission.rao, alice_subnet_netuid), - drain=0, - is_registered=True, - ), ] - stakes = subtensor.get_stake_info_for_coldkey(alice_wallet.coldkey.ss58_address) - - assert stakes == [ - StakeInfo( - hotkey_ss58=alice_wallet.hotkey.ss58_address, - coldkey_ss58=alice_wallet.coldkey.ss58_address, - netuid=alice_subnet_netuid, - stake=get_dynamic_balance(stakes[0].stake.rao, alice_subnet_netuid), - locked=Balance(0).set_unit(alice_subnet_netuid), - emission=get_dynamic_balance(stakes[0].emission.rao, alice_subnet_netuid), - drain=0, - is_registered=True, - ), - StakeInfo( - hotkey_ss58=bob_wallet.hotkey.ss58_address, - coldkey_ss58=alice_wallet.coldkey.ss58_address, - netuid=alice_subnet_netuid, - stake=get_dynamic_balance(stakes[1].stake.rao, alice_subnet_netuid), - locked=Balance(0).set_unit(alice_subnet_netuid), - emission=get_dynamic_balance(stakes[1].emission.rao, alice_subnet_netuid), - drain=0, - is_registered=True, - ), - ] + fast_blocks_stake = ( + [ + StakeInfo( + hotkey_ss58=stakes[1].hotkey_ss58, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=alice_subnet_netuid, + stake=get_dynamic_balance(stakes[1].stake.rao, alice_subnet_netuid), + locked=Balance(0).set_unit(alice_subnet_netuid), + emission=get_dynamic_balance( + stakes[1].emission.rao, alice_subnet_netuid + ), + drain=0, + is_registered=True, + ) + ] + if subtensor.is_fast_blocks() + else [] + ) + + expected_stakes += fast_blocks_stake + + assert stakes == expected_stakes + assert subtensor.get_stake_for_coldkey == subtensor.get_stake_info_for_coldkey stakes = subtensor.get_stake_for_coldkey_and_hotkey( alice_wallet.coldkey.ss58_address, @@ -161,24 +153,27 @@ def test_single_operation(subtensor, alice_wallet, bob_wallet): ), } + # unstale all to check in later success = subtensor.unstake( - alice_wallet, - bob_wallet.hotkey.ss58_address, + wallet=alice_wallet, + hotkey_ss58=bob_wallet.hotkey.ss58_address, netuid=alice_subnet_netuid, - amount=stake_bob, wait_for_inclusion=True, wait_for_finalization=True, + period=16, ) assert success is True stake = subtensor.get_stake( - alice_wallet.coldkey.ss58_address, - bob_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, netuid=alice_subnet_netuid, ) + # all balances have been unstaked assert stake == Balance(0).set_unit(alice_subnet_netuid) + logging.console.success(f"✅ Test [green]test_single_operation[/green] passed") def test_batch_operations(subtensor, alice_wallet, bob_wallet): @@ -300,6 +295,7 @@ def test_batch_operations(subtensor, alice_wallet, bob_wallet): bob_wallet.coldkey.ss58_address: Balance.from_tao(999_998), } assert balances[alice_wallet.coldkey.ss58_address] > alice_balance + logging.console.success(f"✅ Test [green]test_batch_operations[/green] passed") def test_safe_staking_scenarios(subtensor, alice_wallet, bob_wallet): @@ -424,13 +420,17 @@ def test_safe_staking_scenarios(subtensor, alice_wallet, bob_wallet): rate_tolerance=0.005, # 0.5% allow_partial_stake=False, ) - assert success is False + assert success is False, "Unstake should fail." current_stake = subtensor.get_stake( alice_wallet.coldkey.ss58_address, bob_wallet.hotkey.ss58_address, netuid=alice_subnet_netuid, ) + + logging.console.info(f"[orange]Current stake: {current_stake}[orange]") + logging.console.info(f"[orange]Full stake: {full_stake}[orange]") + assert current_stake == full_stake, ( "Stake should not change after failed unstake attempt" ) @@ -454,6 +454,7 @@ def test_safe_staking_scenarios(subtensor, alice_wallet, bob_wallet): bob_wallet.hotkey.ss58_address, netuid=alice_subnet_netuid, ) + logging.console.info(f"[orange]Partial unstake: {partial_unstake}[orange]") assert partial_unstake > Balance(0), "Some stake should remain" # 3. Higher threshold - should succeed fully @@ -468,7 +469,10 @@ def test_safe_staking_scenarios(subtensor, alice_wallet, bob_wallet): rate_tolerance=0.3, # 30% allow_partial_stake=False, ) - assert success is True + assert success is True, "Unstake should succeed" + logging.console.success( + f"✅ Test [green]test_safe_staking_scenarios[/green] passed" + ) def test_safe_swap_stake_scenarios(subtensor, alice_wallet, bob_wallet): @@ -580,6 +584,9 @@ def test_safe_swap_stake_scenarios(subtensor, alice_wallet, bob_wallet): assert dest_stake > Balance(0), ( "Destination stake should be non-zero after successful swap" ) + logging.console.success( + f"✅ Test [green]test_safe_swap_stake_scenarios[/green] passed" + ) def test_move_stake(subtensor, alice_wallet, bob_wallet): @@ -649,29 +656,43 @@ def test_move_stake(subtensor, alice_wallet, bob_wallet): stakes = subtensor.get_stake_for_coldkey(alice_wallet.coldkey.ss58_address) - assert stakes == [ + expected_stakes = [ StakeInfo( - hotkey_ss58=alice_wallet.hotkey.ss58_address, + hotkey_ss58=stakes[0].hotkey_ss58, coldkey_ss58=alice_wallet.coldkey.ss58_address, - netuid=alice_subnet_netuid, + netuid=alice_subnet_netuid + if subtensor.is_fast_blocks() + else bob_subnet_netuid, stake=get_dynamic_balance(stakes[0].stake.rao, bob_subnet_netuid), locked=Balance(0).set_unit(bob_subnet_netuid), emission=get_dynamic_balance(stakes[0].emission.rao, bob_subnet_netuid), drain=0, is_registered=True, - ), - StakeInfo( - hotkey_ss58=bob_wallet.hotkey.ss58_address, - coldkey_ss58=alice_wallet.coldkey.ss58_address, - netuid=bob_subnet_netuid, - stake=get_dynamic_balance(stakes[1].stake.rao, bob_subnet_netuid), - locked=Balance(0).set_unit(bob_subnet_netuid), - emission=get_dynamic_balance(stakes[1].emission.rao, bob_subnet_netuid), - drain=0, - is_registered=True, - ), + ) ] + fast_block_stake = ( + [ + StakeInfo( + hotkey_ss58=stakes[1].hotkey_ss58, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=bob_subnet_netuid, + stake=get_dynamic_balance(stakes[1].stake.rao, bob_subnet_netuid), + locked=Balance(0).set_unit(bob_subnet_netuid), + emission=get_dynamic_balance(stakes[1].emission.rao, bob_subnet_netuid), + drain=0, + is_registered=True, + ), + ] + if subtensor.is_fast_blocks() + else [] + ) + + expected_stakes += fast_block_stake + + assert stakes == expected_stakes + logging.console.success(f"✅ Test [green]test_move_stake[/green] passed") + def test_transfer_stake(subtensor, alice_wallet, bob_wallet, dave_wallet): """ @@ -750,24 +771,32 @@ def test_transfer_stake(subtensor, alice_wallet, bob_wallet, dave_wallet): alice_stakes = subtensor.get_stake_for_coldkey(alice_wallet.coldkey.ss58_address) - assert alice_stakes == [ - StakeInfo( - hotkey_ss58=alice_wallet.hotkey.ss58_address, - coldkey_ss58=alice_wallet.coldkey.ss58_address, - netuid=alice_subnet_netuid, - stake=get_dynamic_balance(alice_stakes[0].stake.rao, alice_subnet_netuid), - locked=Balance(0).set_unit(alice_subnet_netuid), - emission=get_dynamic_balance( - alice_stakes[0].emission.rao, alice_subnet_netuid + expected_alice_stake = ( + [ + StakeInfo( + hotkey_ss58=alice_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=alice_subnet_netuid, + stake=get_dynamic_balance( + alice_stakes[0].stake.rao, alice_subnet_netuid + ), + locked=Balance(0).set_unit(alice_subnet_netuid), + emission=get_dynamic_balance( + alice_stakes[0].emission.rao, alice_subnet_netuid + ), + drain=0, + is_registered=True, ), - drain=0, - is_registered=True, - ), - ] + ] + if subtensor.is_fast_blocks() + else [] + ) + + assert alice_stakes == expected_alice_stake bob_stakes = subtensor.get_stake_for_coldkey(bob_wallet.coldkey.ss58_address) - assert bob_stakes == [ + expected_bob_stake = [ StakeInfo( hotkey_ss58=alice_wallet.hotkey.ss58_address, coldkey_ss58=bob_wallet.coldkey.ss58_address, @@ -781,3 +810,5 @@ def test_transfer_stake(subtensor, alice_wallet, bob_wallet, dave_wallet): is_registered=False, ), ] + assert bob_stakes == expected_bob_stake + logging.console.success(f"✅ Test [green]test_transfer_stake[/green] passed") diff --git a/tests/e2e_tests/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py index c5871d3155..1914f69ac4 100644 --- a/tests/e2e_tests/utils/chain_interactions.py +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -19,7 +19,7 @@ def get_dynamic_balance(rao: int, netuid: int = 0): - """Returns a Balance object with the given rao and netuid for testing purposes with synamic values.""" + """Returns a Balance object with the given rao and netuid for testing purposes with dynamic values.""" return Balance(rao).set_unit(netuid) diff --git a/tests/e2e_tests/utils/e2e_test_utils.py b/tests/e2e_tests/utils/e2e_test_utils.py index a688156c7b..745c8977ab 100644 --- a/tests/e2e_tests/utils/e2e_test_utils.py +++ b/tests/e2e_tests/utils/e2e_test_utils.py @@ -235,6 +235,13 @@ def wait_to_start_call( in_blocks: int = 10, ): """Waits for a certain number of blocks before making a start call.""" + if subtensor.is_fast_blocks() is False: + in_blocks = 5 + bittensor.logging.console.info( + f"Waiting for [blue]{in_blocks}[/blue] blocks before [red]start call[/red]. " + f"Current block: [blue]{subtensor.block}[/blue]." + ) + # make sure we passed start_call limit subtensor.wait_for_block(subtensor.block + in_blocks + 1) status, message = subtensor.start_call( @@ -254,6 +261,14 @@ async def async_wait_to_start_call( in_blocks: int = 10, ): """Waits for a certain number of blocks before making a start call.""" + if await subtensor.is_fast_blocks() is False: + in_blocks = 5 + + bittensor.logging.console.info( + f"Waiting for [blue]{in_blocks}[/blue] blocks before [red]start call[/red]. " + f"Current block: [blue]{subtensor.block}[/blue]." + ) + # make sure we passed start_call limit current_block = await subtensor.block await subtensor.wait_for_block(current_block + in_blocks + 1) diff --git a/tests/integration_tests/test_metagraph_integration.py b/tests/integration_tests/test_metagraph_integration.py index efcb49d267..005b1ae0c3 100644 --- a/tests/integration_tests/test_metagraph_integration.py +++ b/tests/integration_tests/test_metagraph_integration.py @@ -1,10 +1,11 @@ +import os from unittest import mock -import bittensor import torch -import os -from bittensor.utils.mock import MockSubtensor + +import bittensor from bittensor.core.metagraph import METAGRAPH_STATE_DICT_NDARRAY_KEYS, get_save_dir +from bittensor.utils.mock import MockSubtensor _subtensor_mock: MockSubtensor = MockSubtensor() diff --git a/tests/integration_tests/test_subtensor_integration.py b/tests/integration_tests/test_subtensor_integration.py index 46cb382671..0e7c62151a 100644 --- a/tests/integration_tests/test_subtensor_integration.py +++ b/tests/integration_tests/test_subtensor_integration.py @@ -1,10 +1,10 @@ import pytest -from bittensor import NeuronInfo -from bittensor.core.chain_data.axon_info import AxonInfo +from bittensor.core.chain_data import AxonInfo, NeuronInfo from bittensor.core.subtensor import Subtensor from bittensor.utils.balance import Balance from tests.helpers.helpers import FakeWebsocket +from bittensor.utils.mock.subtensor_mock import MockSubtensor @pytest.fixture @@ -40,13 +40,14 @@ async def test_get_all_subnets_info(mocker): assert result[1].blocks_since_epoch == 88 -@pytest.mark.asyncio -async def test_metagraph(mocker): - subtensor = await prepare_test(mocker, "metagraph") - result = subtensor.metagraph(1) - assert result.n == 1 - assert result.netuid == 1 - assert result.block == 3264143 +# TODO: Improve integration tests workflow (https://github.com/opentensor/bittensor/issues/2435#issuecomment-2825858004) +# @pytest.mark.asyncio +# async def test_metagraph(mocker): +# subtensor = await prepare_test(mocker, "metagraph") +# result = subtensor.metagraph(1) +# assert result.n == 1 +# assert result.netuid == 1 +# assert result.block == 3264143 @pytest.mark.asyncio @@ -126,3 +127,27 @@ async def test_get_neuron_for_pubkey_and_subnet(mocker): assert isinstance(result.total_stake, Balance) assert isinstance(result.axon_info, AxonInfo) assert result.is_null is False + + +def test_mock_subtensor_force_register_neuron(): + """Tests the force_register_neuron method of the MockSubtensor class.""" + # Preps + test_netuid = 1 + subtensor = MockSubtensor() + subtensor.create_subnet(netuid=test_netuid) + + uid1 = subtensor.force_register_neuron(test_netuid, "hk1", "cc1") + uid2 = subtensor.force_register_neuron(test_netuid, "hk2", "cc2") + + # Calls + neurons = subtensor.neurons(test_netuid) + neuron1 = subtensor.neuron_for_uid(uid1, test_netuid) + neuron2 = subtensor.neuron_for_uid(uid2, test_netuid) + + # Assertions + assert len(neurons) == 2 + assert [neuron1, neuron2] == neurons + assert neuron1.hotkey == "hk1" + assert neuron1.coldkey == "cc1" + assert neuron2.hotkey == "hk2" + assert neuron2.coldkey == "cc2" diff --git a/tests/unit_tests/extrinsics/asyncex/test_commit_reveal.py b/tests/unit_tests/extrinsics/asyncex/test_commit_reveal.py index 7802ccf9bb..66873eba00 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_commit_reveal.py +++ b/tests/unit_tests/extrinsics/asyncex/test_commit_reveal.py @@ -82,7 +82,7 @@ async def test_do_commit_reveal_v3_success(mocker, subtensor, fake_wallet): wait_for_inclusion=False, wait_for_finalization=False, ) - assert result == (True, "") + assert result == (True, "Not waiting for finalization or inclusion.") @pytest.mark.asyncio @@ -163,7 +163,7 @@ async def test_commit_reveal_v3_extrinsic_success_with_torch( mocked_weights = mocker.Mock() mocked_convert_weights_and_uids_for_emit = mocker.patch.object( async_commit_reveal, - "convert_weights_and_uids_for_emit", + "convert_and_normalize_weights_and_uids", return_value=(mocked_uids, mocked_weights), ) mocked_get_subnet_reveal_period_epochs = mocker.patch.object( @@ -223,6 +223,7 @@ async def test_commit_reveal_v3_extrinsic_success_with_torch( reveal_round=fake_reveal_round, wait_for_inclusion=True, wait_for_finalization=True, + period=None, ) @@ -238,7 +239,7 @@ async def test_commit_reveal_v3_extrinsic_success_with_numpy( mock_convert = mocker.patch.object( async_commit_reveal, - "convert_weights_and_uids_for_emit", + "convert_and_normalize_weights_and_uids", return_value=(fake_uids, fake_weights), ) mock_encode_drand = mocker.patch.object( @@ -288,7 +289,7 @@ async def test_commit_reveal_v3_extrinsic_response_false( # Mocks mocker.patch.object( async_commit_reveal, - "convert_weights_and_uids_for_emit", + "convert_and_normalize_weights_and_uids", return_value=(fake_uids, fake_weights), ) mocker.patch.object( @@ -328,6 +329,7 @@ async def test_commit_reveal_v3_extrinsic_response_false( reveal_round=fake_reveal_round, wait_for_inclusion=True, wait_for_finalization=True, + period=None, ) @@ -341,7 +343,7 @@ async def test_commit_reveal_v3_extrinsic_exception(mocker, subtensor, fake_wall mocker.patch.object( async_commit_reveal, - "convert_weights_and_uids_for_emit", + "convert_and_normalize_weights_and_uids", side_effect=Exception("Test Error"), ) diff --git a/tests/unit_tests/extrinsics/asyncex/test_registration.py b/tests/unit_tests/extrinsics/asyncex/test_registration.py index b53fd946e0..a9b3ac511f 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_registration.py +++ b/tests/unit_tests/extrinsics/asyncex/test_registration.py @@ -16,18 +16,11 @@ async def test_do_pow_register_success(subtensor, fake_wallet, mocker): seal=b"fake_seal", ) - fake_call = mocker.AsyncMock() - fake_extrinsic = mocker.AsyncMock() - fake_response = mocker.Mock() - fake_response.is_success = mocker.AsyncMock(return_value=True)() - fake_response.process_events = mocker.AsyncMock() - - mocker.patch.object(subtensor.substrate, "compose_call", return_value=fake_call) - mocker.patch.object( - subtensor.substrate, "create_signed_extrinsic", return_value=fake_extrinsic - ) + mocker.patch.object(subtensor.substrate, "compose_call") mocker.patch.object( - subtensor.substrate, "submit_extrinsic", return_value=fake_response + subtensor, + "sign_and_send_extrinsic", + new=mocker.AsyncMock(return_value=(True, "")), ) # Call @@ -53,12 +46,12 @@ async def test_do_pow_register_success(subtensor, fake_wallet, mocker): "coldkey": "coldkey_ss58", }, ) - subtensor.substrate.create_signed_extrinsic.assert_awaited_once_with( - call=fake_call, - keypair=fake_wallet.coldkey, - ) - subtensor.substrate.submit_extrinsic.assert_awaited_once_with( - fake_extrinsic, wait_for_inclusion=True, wait_for_finalization=True + subtensor.sign_and_send_extrinsic.assert_awaited_once_with( + call=subtensor.substrate.compose_call.return_value, + wallet=fake_wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + period=None, ) assert result is True assert error_message == "" @@ -75,25 +68,9 @@ async def test_do_pow_register_failure(subtensor, fake_wallet, mocker): nonce=67890, seal=b"fake_seal", ) - fake_err_message = mocker.Mock(autospec=str) - - fake_call = mocker.AsyncMock() - fake_extrinsic = mocker.AsyncMock() - fake_response = mocker.Mock() - fake_response.is_success = mocker.AsyncMock(return_value=False)() - fake_response.process_events = mocker.AsyncMock() - fake_response.error_message = mocker.AsyncMock(return_value=fake_err_message)() - mocker.patch.object(subtensor.substrate, "compose_call", return_value=fake_call) - mocker.patch.object( - subtensor.substrate, "create_signed_extrinsic", return_value=fake_extrinsic - ) - mocker.patch.object( - subtensor.substrate, "submit_extrinsic", return_value=fake_response - ) - mocked_format_error_message = mocker.patch.object( - async_subtensor, "format_error_message" - ) + mocker.patch.object(subtensor.substrate, "compose_call") + mocker.patch.object(subtensor, "sign_and_send_extrinsic") # Call result_error_message = await async_registration._do_pow_register( @@ -118,16 +95,15 @@ async def test_do_pow_register_failure(subtensor, fake_wallet, mocker): "coldkey": "coldkey_ss58", }, ) - subtensor.substrate.create_signed_extrinsic.assert_awaited_once_with( - call=fake_call, - keypair=fake_wallet.coldkey, - ) - subtensor.substrate.submit_extrinsic.assert_awaited_once_with( - fake_extrinsic, wait_for_inclusion=True, wait_for_finalization=True + subtensor.sign_and_send_extrinsic.assert_awaited_once_with( + call=subtensor.substrate.compose_call.return_value, + wallet=fake_wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + period=None, ) - mocked_format_error_message.assert_called_once_with(fake_err_message) - assert result_error_message == (False, mocked_format_error_message.return_value) + assert result_error_message == subtensor.sign_and_send_extrinsic.return_value @pytest.mark.asyncio @@ -142,20 +118,11 @@ async def test_do_pow_register_no_waiting(subtensor, fake_wallet, mocker): seal=b"fake_seal", ) - fake_call = mocker.AsyncMock() - fake_extrinsic = mocker.AsyncMock() - fake_response = mocker.Mock() - - mocker.patch.object(subtensor.substrate, "compose_call", return_value=fake_call) - mocker.patch.object( - subtensor.substrate, "create_signed_extrinsic", return_value=fake_extrinsic - ) - mocker.patch.object( - subtensor.substrate, "submit_extrinsic", return_value=fake_response - ) + mocker.patch.object(subtensor.substrate, "compose_call") + mocker.patch.object(subtensor, "sign_and_send_extrinsic") # Call - result, error_message = await async_registration._do_pow_register( + result = await async_registration._do_pow_register( subtensor=subtensor, netuid=1, wallet=fake_wallet, @@ -177,15 +144,15 @@ async def test_do_pow_register_no_waiting(subtensor, fake_wallet, mocker): "coldkey": "coldkey_ss58", }, ) - subtensor.substrate.create_signed_extrinsic.assert_awaited_once_with( - call=fake_call, - keypair=fake_wallet.coldkey, - ) - subtensor.substrate.submit_extrinsic.assert_awaited_once_with( - fake_extrinsic, wait_for_inclusion=False, wait_for_finalization=False + subtensor.sign_and_send_extrinsic.assert_awaited_once_with( + call=subtensor.substrate.compose_call.return_value, + wallet=fake_wallet, + wait_for_inclusion=False, + wait_for_finalization=False, + period=None, ) - assert result is True - assert error_message == "" + + assert result == subtensor.sign_and_send_extrinsic.return_value @pytest.mark.asyncio @@ -450,6 +417,7 @@ async def is_stale_side_effect(*_, **__): pow_result=fake_pow_result, wait_for_inclusion=True, wait_for_finalization=True, + period=None, ) assert result is False @@ -510,6 +478,7 @@ async def test_set_subnet_identity_extrinsic_is_success(subtensor, fake_wallet, wallet=fake_wallet, wait_for_inclusion=False, wait_for_finalization=True, + period=None, ) assert result == (True, "Identities for subnet 123 are set.") @@ -574,6 +543,7 @@ async def test_set_subnet_identity_extrinsic_is_failed(subtensor, fake_wallet, m wallet=fake_wallet, wait_for_inclusion=True, wait_for_finalization=True, + period=None, ) assert result == ( diff --git a/tests/unit_tests/extrinsics/asyncex/test_root.py b/tests/unit_tests/extrinsics/asyncex/test_root.py index ecca5a9847..baa134ee92 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_root.py +++ b/tests/unit_tests/extrinsics/asyncex/test_root.py @@ -332,7 +332,7 @@ async def test_do_set_root_weights_success(subtensor, fake_wallet, mocker): fake_weights = [0.1, 0.2, 0.7] fake_call = mocker.AsyncMock() - fake_extrinsic = mocker.AsyncMock() + fake_extrinsic = True, "Successfully set weights." fake_response = mocker.Mock() fake_response.is_success = mocker.AsyncMock(return_value=True)() @@ -340,10 +340,7 @@ async def test_do_set_root_weights_success(subtensor, fake_wallet, mocker): mocker.patch.object(subtensor.substrate, "compose_call", return_value=fake_call) mocker.patch.object( - subtensor.substrate, "create_signed_extrinsic", return_value=fake_extrinsic - ) - mocker.patch.object( - subtensor.substrate, "submit_extrinsic", return_value=fake_response + subtensor, "sign_and_send_extrinsic", return_value=fake_extrinsic ) # Call @@ -369,14 +366,13 @@ async def test_do_set_root_weights_success(subtensor, fake_wallet, mocker): "hotkey": "fake_hotkey_address", }, ) - subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + subtensor.sign_and_send_extrinsic.assert_called_once_with( call=fake_call, - keypair=fake_wallet.coldkey, - era={"period": 5}, - nonce=subtensor.substrate.get_account_next_index.return_value, - ) - subtensor.substrate.submit_extrinsic.assert_called_once_with( - extrinsic=fake_extrinsic, wait_for_inclusion=True, wait_for_finalization=True + wallet=fake_wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + use_nonce=True, + period=8, ) assert result is True assert message == "Successfully set weights." @@ -391,28 +387,10 @@ async def test_do_set_root_weights_failure(subtensor, fake_wallet, mocker): fake_weights = [0.1, 0.2, 0.7] fake_call = mocker.AsyncMock() - fake_extrinsic = mocker.AsyncMock() - fake_response = mocker.Mock() - - async def fake_is_success(): - return False - - fake_response.is_success = fake_is_success() - fake_response.process_events = mocker.AsyncMock() - fake_response.error_message = mocker.AsyncMock()() mocker.patch.object(subtensor.substrate, "compose_call", return_value=fake_call) mocker.patch.object( - subtensor.substrate, "create_signed_extrinsic", return_value=fake_extrinsic - ) - mocker.patch.object( - subtensor.substrate, "submit_extrinsic", return_value=fake_response - ) - - mocked_format_error_message = mocker.patch.object( - async_root, - "format_error_message", - return_value="Transaction failed", + subtensor, "sign_and_send_extrinsic", return_value=(False, "Transaction failed") ) # Call @@ -428,7 +406,6 @@ async def fake_is_success(): # Asserts assert result is False - assert message == mocked_format_error_message.return_value @pytest.mark.asyncio @@ -441,14 +418,12 @@ async def test_do_set_root_weights_no_waiting(subtensor, fake_wallet, mocker): fake_call = mocker.AsyncMock() fake_extrinsic = mocker.AsyncMock() - fake_response = mocker.Mock() mocker.patch.object(subtensor.substrate, "compose_call", return_value=fake_call) mocker.patch.object( - subtensor.substrate, "create_signed_extrinsic", return_value=fake_extrinsic - ) - mocker.patch.object( - subtensor.substrate, "submit_extrinsic", return_value=fake_response + subtensor, + "sign_and_send_extrinsic", + return_value=(True, "Not waiting for finalization or inclusion."), ) # Call @@ -464,9 +439,13 @@ async def test_do_set_root_weights_no_waiting(subtensor, fake_wallet, mocker): # Asserts subtensor.substrate.compose_call.assert_called_once() - subtensor.substrate.create_signed_extrinsic.assert_called_once() - subtensor.substrate.submit_extrinsic.assert_called_once_with( - extrinsic=fake_extrinsic, wait_for_inclusion=False, wait_for_finalization=False + subtensor.sign_and_send_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, + wallet=fake_wallet, + wait_for_inclusion=False, + wait_for_finalization=False, + use_nonce=True, + period=8, ) assert result is True assert message == "Not waiting for finalization or inclusion." @@ -674,5 +653,6 @@ async def test_set_root_weights_extrinsic_request_exception( version_key=0, wait_for_inclusion=True, wait_for_finalization=True, + period=None, ) mocked_format_error_message.assert_called_once() diff --git a/tests/unit_tests/extrinsics/asyncex/test_start_call.py b/tests/unit_tests/extrinsics/asyncex/test_start_call.py index 63083cd094..d99295195e 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_start_call.py +++ b/tests/unit_tests/extrinsics/asyncex/test_start_call.py @@ -5,18 +5,15 @@ @pytest.mark.asyncio async def test_start_call_extrinsics(subtensor, mocker, fake_wallet): """Test that start_call_extrinsic correctly constructs and submits the extrinsic.""" - # Preps netuid = 123 wallet = fake_wallet wallet.name = "fake_wallet" wallet.coldkey = "fake_coldkey" - substrate = subtensor.substrate.__aenter__.return_value - substrate.compose_call.return_value = "mock_call" - substrate.create_signed_extrinsic.return_value = "signed_ext" - substrate.submit_extrinsic.return_value = mocker.MagicMock( - is_success=mocker.AsyncMock(return_value=True)(), error_message="" + substrate.compose_call = mocker.AsyncMock() + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic", return_value=(True, "") ) # Call @@ -33,15 +30,12 @@ async def test_start_call_extrinsics(subtensor, mocker, fake_wallet): call_params={"netuid": netuid}, ) - substrate.create_signed_extrinsic.assert_awaited_once_with( - call="mock_call", - keypair=wallet.coldkey, - ) - - substrate.submit_extrinsic.assert_awaited_once_with( - extrinsic="signed_ext", + mocked_sign_and_send_extrinsic.assert_awaited_once_with( + call=substrate.compose_call.return_value, + wallet=wallet, wait_for_inclusion=True, wait_for_finalization=False, + period=None, ) assert success is True diff --git a/tests/unit_tests/extrinsics/asyncex/test_transfer.py b/tests/unit_tests/extrinsics/asyncex/test_transfer.py index 0fa70a8e75..77fcd72f9f 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_transfer.py +++ b/tests/unit_tests/extrinsics/asyncex/test_transfer.py @@ -9,22 +9,15 @@ async def test_do_transfer_success(subtensor, fake_wallet, mocker): # Preps fake_destination = "destination_address" fake_amount = mocker.Mock(autospec=Balance, rao=1000) + fake_block_hash = "fake_block_hash" - fake_call = mocker.AsyncMock() - fake_extrinsic = mocker.AsyncMock() - fake_response = mocker.Mock() - - fake_response.is_success = mocker.AsyncMock(return_value=True)() - fake_response.process_events = mocker.AsyncMock() - fake_response.block_hash = "fake_block_hash" - - mocker.patch.object(subtensor.substrate, "compose_call", return_value=fake_call) - mocker.patch.object( - subtensor.substrate, "create_signed_extrinsic", return_value=fake_extrinsic - ) + mocker.patch.object(subtensor.substrate, "compose_call") mocker.patch.object( - subtensor.substrate, "submit_extrinsic", return_value=fake_response + subtensor, + "sign_and_send_extrinsic", + new=mocker.AsyncMock(return_value=(True, "")), ) + mocker.patch.object(subtensor, "get_block_hash", return_value=fake_block_hash) # Call success, block_hash, error_message = await async_transfer._do_transfer( @@ -37,18 +30,18 @@ async def test_do_transfer_success(subtensor, fake_wallet, mocker): ) # Asserts - subtensor.substrate.compose_call.assert_called_once_with( + subtensor.substrate.compose_call.assert_awaited_once_with( call_module="Balances", call_function="transfer_allow_death", call_params={"dest": fake_destination, "value": fake_amount.rao}, ) - subtensor.substrate.create_signed_extrinsic.assert_called_once_with( - call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.coldkey - ) - subtensor.substrate.submit_extrinsic.assert_called_once_with( - extrinsic=subtensor.substrate.create_signed_extrinsic.return_value, + + subtensor.sign_and_send_extrinsic.assert_awaited_once_with( + call=subtensor.substrate.compose_call.return_value, + wallet=fake_wallet, wait_for_inclusion=True, wait_for_finalization=True, + period=None, ) assert success is True assert block_hash == "fake_block_hash" @@ -61,28 +54,15 @@ async def test_do_transfer_failure(subtensor, fake_wallet, mocker): # Preps fake_destination = "destination_address" fake_amount = mocker.Mock(autospec=Balance, rao=1000) + fake_block_hash = "fake_block_hash" - fake_call = mocker.AsyncMock() - fake_extrinsic = mocker.AsyncMock() - fake_response = mocker.Mock() - - fake_response.is_success = mocker.AsyncMock(return_value=False)() - fake_response.process_events = mocker.AsyncMock() - fake_response.error_message = mocker.AsyncMock(return_value="Fake error message")() - - mocker.patch.object(subtensor.substrate, "compose_call", return_value=fake_call) + mocker.patch.object(subtensor.substrate, "compose_call") mocker.patch.object( - subtensor.substrate, "create_signed_extrinsic", return_value=fake_extrinsic - ) - mocker.patch.object( - subtensor.substrate, "submit_extrinsic", return_value=fake_response - ) - - mocked_format_error_message = mocker.patch.object( - async_transfer, - "format_error_message", - return_value="Formatted error message", + subtensor, + "sign_and_send_extrinsic", + new=mocker.AsyncMock(return_value=(False, "Formatted error message")), ) + mocker.patch.object(subtensor, "get_block_hash", return_value=fake_block_hash) # Call success, block_hash, error_message = await async_transfer._do_transfer( @@ -95,22 +75,21 @@ async def test_do_transfer_failure(subtensor, fake_wallet, mocker): ) # Asserts - subtensor.substrate.compose_call.assert_called_once_with( + subtensor.substrate.compose_call.assert_awaited_once_with( call_module="Balances", call_function="transfer_allow_death", call_params={"dest": fake_destination, "value": fake_amount.rao}, ) - subtensor.substrate.create_signed_extrinsic.assert_called_once_with( - call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.coldkey - ) - subtensor.substrate.submit_extrinsic.assert_called_once_with( - extrinsic=subtensor.substrate.create_signed_extrinsic.return_value, + + subtensor.sign_and_send_extrinsic.assert_awaited_once_with( + call=subtensor.substrate.compose_call.return_value, + wallet=fake_wallet, wait_for_inclusion=True, wait_for_finalization=True, + period=None, ) assert success is False assert block_hash == "" - mocked_format_error_message.assert_called_once_with("Fake error message") assert error_message == "Formatted error message" @@ -120,19 +99,17 @@ async def test_do_transfer_no_waiting(subtensor, fake_wallet, mocker): # Preps fake_destination = "destination_address" fake_amount = mocker.Mock(autospec=Balance, rao=1000) + fake_block_hash = "fake_block_hash" - fake_call = mocker.AsyncMock() - fake_extrinsic = mocker.AsyncMock() - - mocker.patch.object(subtensor.substrate, "compose_call", return_value=fake_call) + mocker.patch.object(subtensor.substrate, "compose_call") mocker.patch.object( - subtensor.substrate, "create_signed_extrinsic", return_value=fake_extrinsic - ) - mocker.patch.object( - subtensor.substrate, - "submit_extrinsic", - return_value=mocker.Mock(), + subtensor, + "sign_and_send_extrinsic", + new=mocker.AsyncMock( + return_value=(False, "Success, extrinsic submitted without waiting.") + ), ) + mocker.patch.object(subtensor, "get_block_hash", return_value=fake_block_hash) # Call success, block_hash, error_message = await async_transfer._do_transfer( @@ -145,18 +122,18 @@ async def test_do_transfer_no_waiting(subtensor, fake_wallet, mocker): ) # Asserts - subtensor.substrate.compose_call.assert_called_once_with( + subtensor.substrate.compose_call.assert_awaited_once_with( call_module="Balances", call_function="transfer_allow_death", call_params={"dest": fake_destination, "value": fake_amount.rao}, ) - subtensor.substrate.create_signed_extrinsic.assert_called_once_with( - call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.coldkey - ) - subtensor.substrate.submit_extrinsic.assert_called_once_with( - extrinsic=subtensor.substrate.create_signed_extrinsic.return_value, + + subtensor.sign_and_send_extrinsic.assert_awaited_once_with( + call=subtensor.substrate.compose_call.return_value, + wallet=fake_wallet, wait_for_inclusion=False, wait_for_finalization=False, + period=None, ) assert success is True assert block_hash == "" diff --git a/tests/unit_tests/extrinsics/asyncex/test_weights.py b/tests/unit_tests/extrinsics/asyncex/test_weights.py index c01490055d..226292b43c 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_weights.py +++ b/tests/unit_tests/extrinsics/asyncex/test_weights.py @@ -43,7 +43,7 @@ async def fake_is_success(): # Asserts assert result is True - assert message == "" + assert message == "Successfully set weights." @pytest.mark.asyncio @@ -131,7 +131,7 @@ async def test_do_set_weights_no_waiting(subtensor, fake_wallet, mocker): # Asserts assert result is True - assert message == "" + assert message == "Not waiting for finalization or inclusion." @pytest.mark.asyncio @@ -141,12 +141,16 @@ async def test_set_weights_extrinsic_success_with_finalization( """Tests set_weights_extrinsic when weights are successfully set with finalization.""" # Preps fake_netuid = 1 - fake_uids = [1, 2, 3] - fake_weights = [0.1, 0.2, 0.7] + fake_uids = mocker.Mock() + fake_weights = mocker.Mock() mocked_do_set_weights = mocker.patch.object( async_weights, "_do_set_weights", return_value=(True, "") ) + mocker_converter = mocker.patch.object( + async_weights, "convert_and_normalize_weights_and_uids" + ) + mocker_converter.return_value = (mocker.Mock(), mocker.Mock()) # Call result, message = await async_weights.set_weights_extrinsic( @@ -160,16 +164,18 @@ async def test_set_weights_extrinsic_success_with_finalization( ) # Asserts + mocker_converter.assert_called_once_with(fake_uids, fake_weights) + mocked_do_set_weights.assert_called_once_with( subtensor=subtensor, wallet=fake_wallet, netuid=fake_netuid, - uids=mocker.ANY, - vals=mocker.ANY, + uids=mocker_converter.return_value[0], + vals=mocker_converter.return_value[1], version_key=0, wait_for_finalization=True, wait_for_inclusion=True, - period=5, + period=8, ) assert result is True assert message == "Successfully set weights and Finalized." @@ -184,7 +190,9 @@ async def test_set_weights_extrinsic_no_waiting(subtensor, fake_wallet, mocker): fake_weights = [0.1, 0.2, 0.7] mocked_do_set_weights = mocker.patch.object( - async_weights, "_do_set_weights", return_value=(True, "") + async_weights, + "_do_set_weights", + return_value=(True, "Not waiting for finalization or inclusion."), ) # Call @@ -380,7 +388,7 @@ async def test_do_commit_weights_no_waiting(subtensor, fake_wallet, mocker): # Asserts assert result is True - assert message == "" + assert message == "Not waiting for finalization or inclusion." @pytest.mark.asyncio @@ -437,6 +445,7 @@ async def test_commit_weights_extrinsic_success(subtensor, fake_wallet, mocker): commit_hash=fake_commit_hash, wait_for_inclusion=True, wait_for_finalization=True, + period=None, ) assert result is True assert message == "✅ [green]Successfully committed weights.[green]" @@ -471,6 +480,7 @@ async def test_commit_weights_extrinsic_failure(subtensor, fake_wallet, mocker): commit_hash=fake_commit_hash, wait_for_inclusion=True, wait_for_finalization=True, + period=None, ) assert result is False assert message == "Commit failed." diff --git a/tests/unit_tests/extrinsics/test__init__.py b/tests/unit_tests/extrinsics/test__init__.py index 1c087a781b..9eb994a2a3 100644 --- a/tests/unit_tests/extrinsics/test__init__.py +++ b/tests/unit_tests/extrinsics/test__init__.py @@ -4,12 +4,16 @@ def test_format_error_message_with_right_error_message(): - """Verify that error message from extrinsic response parses correctly.""" + """Verify that an error message from extrinsic response parses correctly.""" # Prep fake_error_message = { "type": "SomeType", "name": "SomeErrorName", - "docs": ["Some error description."], + "docs": [ + "Some error description.", + "I'm second part.", + "Hah, I'm the last one.", + ], } # Call @@ -17,13 +21,14 @@ def test_format_error_message_with_right_error_message(): # Assertions - assert "SomeType" in result - assert "SomeErrorName" in result - assert "Some error description." in result + assert ( + result + == "Subtensor returned `SomeErrorName(SomeType)` error. This means: `Some error description. I'm second part. Hah, I'm the last one.`." + ) def test_format_error_message_with_empty_error_message(): - """Verify that empty error message from extrinsic response parses correctly.""" + """Verify that an empty error message from extrinsic response parses correctly.""" # Prep fake_error_message = {} diff --git a/tests/unit_tests/extrinsics/test_commit_reveal.py b/tests/unit_tests/extrinsics/test_commit_reveal.py index 37b131e391..3891b97dae 100644 --- a/tests/unit_tests/extrinsics/test_commit_reveal.py +++ b/tests/unit_tests/extrinsics/test_commit_reveal.py @@ -48,11 +48,8 @@ def test_do_commit_reveal_v3_success(mocker, subtensor, fake_wallet): fake_reveal_round = 1 mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") - mocked_create_signed_extrinsic = mocker.patch.object( - subtensor.substrate, "create_signed_extrinsic" - ) - mocked_submit_extrinsic = mocker.patch.object( - subtensor.substrate, "submit_extrinsic" + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic", return_value=(True, "") ) # Call @@ -74,14 +71,15 @@ def test_do_commit_reveal_v3_success(mocker, subtensor, fake_wallet): "reveal_round": fake_reveal_round, }, ) - mocked_create_signed_extrinsic.assert_called_once_with( - call=mocked_compose_call.return_value, keypair=fake_wallet.hotkey - ) - mocked_submit_extrinsic.assert_called_once_with( - mocked_create_signed_extrinsic.return_value, + mocked_sign_and_send_extrinsic.assert_called_once_with( + call=mocked_compose_call.return_value, + wallet=fake_wallet, wait_for_inclusion=False, wait_for_finalization=False, + sign_with="hotkey", + period=None, ) + assert result == (True, "") @@ -155,7 +153,7 @@ def test_commit_reveal_v3_extrinsic_success_with_torch( mocked_weights = mocker.Mock() mocked_convert_weights_and_uids_for_emit = mocker.patch.object( commit_reveal, - "convert_weights_and_uids_for_emit", + "convert_and_normalize_weights_and_uids", return_value=(mocked_uids, mocked_weights), ) mocker.patch.object(subtensor, "get_subnet_reveal_period_epochs") @@ -183,6 +181,7 @@ def test_commit_reveal_v3_extrinsic_success_with_torch( weights=fake_weights, wait_for_inclusion=True, wait_for_finalization=True, + period=None, ) # Asserts @@ -209,6 +208,7 @@ def test_commit_reveal_v3_extrinsic_success_with_torch( reveal_round=fake_reveal_round, wait_for_inclusion=True, wait_for_finalization=True, + period=None, ) @@ -223,7 +223,7 @@ def test_commit_reveal_v3_extrinsic_success_with_numpy( mock_convert = mocker.patch.object( commit_reveal, - "convert_weights_and_uids_for_emit", + "convert_and_normalize_weights_and_uids", return_value=(fake_uids, fake_weights), ) mock_encode_drand = mocker.patch.object( @@ -272,7 +272,7 @@ def test_commit_reveal_v3_extrinsic_response_false( # Mocks mocker.patch.object( commit_reveal, - "convert_weights_and_uids_for_emit", + "convert_and_normalize_weights_and_uids", return_value=(fake_uids, fake_weights), ) mocker.patch.object( @@ -312,6 +312,7 @@ def test_commit_reveal_v3_extrinsic_response_false( reveal_round=fake_reveal_round, wait_for_inclusion=True, wait_for_finalization=True, + period=None, ) @@ -324,7 +325,7 @@ def test_commit_reveal_v3_extrinsic_exception(mocker, subtensor, fake_wallet): mocker.patch.object( commit_reveal, - "convert_weights_and_uids_for_emit", + "convert_and_normalize_weights_and_uids", side_effect=Exception("Test Error"), ) diff --git a/tests/unit_tests/extrinsics/test_commit_weights.py b/tests/unit_tests/extrinsics/test_commit_weights.py index 71771d2247..e27547528b 100644 --- a/tests/unit_tests/extrinsics/test_commit_weights.py +++ b/tests/unit_tests/extrinsics/test_commit_weights.py @@ -13,7 +13,8 @@ def test_do_commit_weights(subtensor, fake_wallet, mocker): wait_for_inclusion = True wait_for_finalization = True - subtensor.substrate.submit_extrinsic.return_value.is_success = None + mocker.patch.object(subtensor, "sign_and_send_extrinsic", return_value=(False, "")) + mocker.patch.object(subtensor, "get_block_hash", return_value=1) mocked_format_error_message = mocker.Mock() mocker.patch( @@ -41,25 +42,18 @@ def test_do_commit_weights(subtensor, fake_wallet, mocker): }, ) - subtensor.substrate.create_signed_extrinsic.assert_called_once() - _, kwargs = subtensor.substrate.create_signed_extrinsic.call_args - assert kwargs["call"] == subtensor.substrate.compose_call.return_value - assert kwargs["keypair"] == fake_wallet.hotkey - - subtensor.substrate.submit_extrinsic.assert_called_once_with( - subtensor.substrate.create_signed_extrinsic.return_value, + subtensor.sign_and_send_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, + wallet=fake_wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=None, + nonce_key="hotkey", + sign_with="hotkey", + use_nonce=True, ) - mocked_format_error_message.assert_called_once_with( - subtensor.substrate.submit_extrinsic.return_value.error_message, - ) - - assert result == ( - False, - mocked_format_error_message.return_value, - ) + assert result == (False, "") def test_do_reveal_weights(subtensor, fake_wallet, mocker): @@ -74,13 +68,7 @@ def test_do_reveal_weights(subtensor, fake_wallet, mocker): wait_for_inclusion = True wait_for_finalization = True - subtensor.substrate.submit_extrinsic.return_value.is_success = None - - mocked_format_error_message = mocker.Mock() - mocker.patch( - "bittensor.core.subtensor.format_error_message", - mocked_format_error_message, - ) + mocker.patch.object(subtensor, "sign_and_send_extrinsic") # Call result = _do_reveal_weights( @@ -108,23 +96,15 @@ def test_do_reveal_weights(subtensor, fake_wallet, mocker): }, ) - subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + subtensor.sign_and_send_extrinsic( call=subtensor.substrate.compose_call.return_value, - keypair=fake_wallet.hotkey, - nonce=subtensor.substrate.get_account_next_index.return_value, - ) - - subtensor.substrate.submit_extrinsic.assert_called_once_with( - subtensor.substrate.create_signed_extrinsic.return_value, + wallet=fake_wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=None, + nonce_key="hotkey", + sign_with="hotkey", + use_nonce=True, ) - mocked_format_error_message.assert_called_once_with( - subtensor.substrate.submit_extrinsic.return_value.error_message, - ) - - assert result == ( - False, - mocked_format_error_message.return_value, - ) + assert result == subtensor.sign_and_send_extrinsic.return_value diff --git a/tests/unit_tests/extrinsics/test_registration.py b/tests/unit_tests/extrinsics/test_registration.py index b2d816d4dd..c45bfd6214 100644 --- a/tests/unit_tests/extrinsics/test_registration.py +++ b/tests/unit_tests/extrinsics/test_registration.py @@ -80,40 +80,37 @@ def test_register_extrinsic_without_pow( mocker, ): # Arrange - with ( - mocker.patch.object( - mock_subtensor, "subnet_exists", return_value=subnet_exists - ), - mocker.patch.object( - mock_subtensor, - "get_neuron_for_pubkey_and_subnet", - return_value=mocker.MagicMock(is_null=neuron_is_null), - ), - mocker.patch("torch.cuda.is_available", return_value=cuda_available), - mocker.patch( - "bittensor.utils.registration.pow._get_block_with_retry", - return_value=(0, 0, "00ff11ee"), - ), - ): - # Act - result = registration.register_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - netuid=123, - wait_for_inclusion=True, - wait_for_finalization=True, - max_allowed_attempts=3, - output_in_place=True, - cuda=True, - dev_id=0, - tpb=256, - num_processes=None, - update_interval=None, - log_verbose=False, - ) + mocker.patch.object(mock_subtensor, "subnet_exists", return_value=subnet_exists) + mocker.patch.object( + mock_subtensor, + "get_neuron_for_pubkey_and_subnet", + return_value=mocker.MagicMock(is_null=neuron_is_null), + ) + mocker.patch("torch.cuda.is_available", return_value=cuda_available) + mocker.patch( + "bittensor.utils.registration.pow._get_block_with_retry", + return_value=(0, 0, "00ff11ee"), + ) - # Assert - assert result == expected_result, f"Test failed for test_id: {test_id}" + # Act + result = registration.register_extrinsic( + subtensor=mock_subtensor, + wallet=mock_wallet, + netuid=123, + wait_for_inclusion=True, + wait_for_finalization=True, + max_allowed_attempts=3, + output_in_place=True, + cuda=True, + dev_id=0, + tpb=256, + num_processes=None, + update_interval=None, + log_verbose=False, + ) + + # Assert + assert result == expected_result, f"Test failed for test_id: {test_id}" @pytest.mark.parametrize( @@ -141,48 +138,47 @@ def test_register_extrinsic_with_pow( mocker, ): # Arrange - with ( - mocker.patch( - "bittensor.utils.registration.pow._solve_for_difficulty_fast", - return_value=mock_pow_solution if pow_success else None, - ), - mocker.patch( - "bittensor.utils.registration.pow._solve_for_difficulty_fast_cuda", - return_value=mock_pow_solution if pow_success else None, - ), - mocker.patch( - "bittensor.core.extrinsics.registration._do_pow_register", - return_value=(registration_success, "HotKeyAlreadyRegisteredInSubNet"), - ), - mocker.patch("torch.cuda.is_available", return_value=cuda), - ): - # Act - if pow_success: - mock_pow_solution.is_stale.return_value = pow_stale - - if not pow_success and hotkey_registered: - mock_subtensor.is_hotkey_registered = mocker.MagicMock( - return_value=hotkey_registered - ) - - result = registration.register_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - netuid=123, - wait_for_inclusion=True, - wait_for_finalization=True, - max_allowed_attempts=3, - output_in_place=True, - cuda=cuda, - dev_id=0, - tpb=256, - num_processes=None, - update_interval=None, - log_verbose=False, + mocker.patch( + "bittensor.utils.registration.pow._solve_for_difficulty_fast", + return_value=mock_pow_solution if pow_success else None, + ) + mocker.patch( + "bittensor.utils.registration.pow._solve_for_difficulty_fast_cuda", + return_value=mock_pow_solution if pow_success else None, + ) + mocker.patch( + "bittensor.core.extrinsics.registration._do_pow_register", + return_value=(registration_success, "HotKeyAlreadyRegisteredInSubNet"), + ) + mocker.patch("torch.cuda.is_available", return_value=cuda) + + # Act + if pow_success: + mock_pow_solution.is_stale.return_value = pow_stale + + if not pow_success and hotkey_registered: + mock_subtensor.is_hotkey_registered = mocker.MagicMock( + return_value=hotkey_registered ) - # Assert - assert result == expected_result, f"Test failed for test_id: {test_id}." + result = registration.register_extrinsic( + subtensor=mock_subtensor, + wallet=mock_wallet, + netuid=123, + wait_for_inclusion=True, + wait_for_finalization=True, + max_allowed_attempts=3, + output_in_place=True, + cuda=cuda, + dev_id=0, + tpb=256, + num_processes=None, + update_interval=None, + log_verbose=False, + ) + + # Assert + assert result == expected_result, f"Test failed for test_id: {test_id}." @pytest.mark.parametrize( @@ -209,29 +205,26 @@ def test_burned_register_extrinsic( mocker, ): # Arrange - with ( - mocker.patch.object( - mock_subtensor, "subnet_exists", return_value=subnet_exists - ), - mocker.patch.object( - mock_subtensor, - "get_neuron_for_pubkey_and_subnet", - return_value=mocker.MagicMock(is_null=neuron_is_null), - ), - mocker.patch( - "bittensor.core.extrinsics.registration._do_burned_register", - return_value=(recycle_success, "Mock error message"), - ), - mocker.patch.object( - mock_subtensor, "is_hotkey_registered", return_value=is_registered - ), - ): - # Act - result = registration.burned_register_extrinsic( - subtensor=mock_subtensor, wallet=mock_wallet, netuid=123 - ) - # Assert - assert result == expected_result, f"Test failed for test_id: {test_id}" + mocker.patch.object(mock_subtensor, "subnet_exists", return_value=subnet_exists) + mocker.patch.object( + mock_subtensor, + "get_neuron_for_pubkey_and_subnet", + return_value=mocker.MagicMock(is_null=neuron_is_null), + ) + mocker.patch( + "bittensor.core.extrinsics.registration._do_burned_register", + return_value=(recycle_success, "Mock error message"), + ) + mocker.patch.object( + mock_subtensor, "is_hotkey_registered", return_value=is_registered + ) + + # Act + result = registration.burned_register_extrinsic( + subtensor=mock_subtensor, wallet=mock_wallet, netuid=123 + ) + # Assert + assert result == expected_result, f"Test failed for test_id: {test_id}" def test_set_subnet_identity_extrinsic_is_success(mock_subtensor, mock_wallet, mocker): @@ -286,6 +279,7 @@ def test_set_subnet_identity_extrinsic_is_success(mock_subtensor, mock_wallet, m wallet=mock_wallet, wait_for_inclusion=False, wait_for_finalization=True, + period=None, ) assert result == (True, "Identities for subnet 123 are set.") @@ -347,6 +341,7 @@ def test_set_subnet_identity_extrinsic_is_failed(mock_subtensor, mock_wallet, mo wallet=mock_wallet, wait_for_inclusion=False, wait_for_finalization=True, + period=None, ) assert result == ( diff --git a/tests/unit_tests/extrinsics/test_root.py b/tests/unit_tests/extrinsics/test_root.py index a22d456a2e..8737b3f32c 100644 --- a/tests/unit_tests/extrinsics/test_root.py +++ b/tests/unit_tests/extrinsics/test_root.py @@ -99,10 +99,11 @@ def test_root_register_extrinsic( call_params={"hotkey": "fake_hotkey_address"}, ) mocked_sign_and_send_extrinsic.assert_called_once_with( - mock_subtensor.substrate.compose_call.return_value, + call=mock_subtensor.substrate.compose_call.return_value, wallet=mock_wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=None, ) @@ -125,7 +126,7 @@ def test_root_register_extrinsic_insufficient_balance( assert success is False mock_subtensor.get_balance.assert_called_once_with( - mock_wallet.coldkeypub.ss58_address, + address=mock_wallet.coldkeypub.ss58_address, block=mock_subtensor.get_current_block.return_value, ) mock_subtensor.substrate.submit_extrinsic.assert_not_called() diff --git a/tests/unit_tests/extrinsics/test_serving.py b/tests/unit_tests/extrinsics/test_serving.py index 8fd01ef6ef..c1688fd3d4 100644 --- a/tests/unit_tests/extrinsics/test_serving.py +++ b/tests/unit_tests/extrinsics/test_serving.py @@ -321,7 +321,7 @@ def test_serve_axon_extrinsic( 1, "Sha256", b"mock_bytes_data", - True, + (True, ""), True, "happy-path-wait", ), @@ -331,7 +331,7 @@ def test_serve_axon_extrinsic( 1, "Sha256", b"mock_bytes_data", - True, + (True, ""), True, "happy-path-no-wait", ), @@ -353,15 +353,8 @@ def test_publish_metadata( # Arrange with ( patch.object(mock_subtensor.substrate, "compose_call"), - patch.object(mock_subtensor.substrate, "create_signed_extrinsic"), patch.object( - mock_subtensor.substrate, - "submit_extrinsic", - return_value=MagicMock( - is_success=response_success, - process_events=MagicMock(), - error_message="error", - ), + mock_subtensor, "sign_and_send_extrinsic", return_value=response_success ), ): # Act diff --git a/tests/unit_tests/extrinsics/test_set_weights.py b/tests/unit_tests/extrinsics/test_set_weights.py index d3ebfd71c8..1046385291 100644 --- a/tests/unit_tests/extrinsics/test_set_weights.py +++ b/tests/unit_tests/extrinsics/test_set_weights.py @@ -68,16 +68,16 @@ def test_set_weights_extrinsic( expected_success, expected_message, ): - uids_tensor = torch.tensor(uids, dtype=torch.int64) - weights_tensor = torch.tensor(weights, dtype=torch.float32) + # uids_tensor = torch.tensor(uids, dtype=torch.int64) + # weights_tensor = torch.tensor(weights, dtype=torch.float32) with ( patch( "bittensor.utils.weight_utils.convert_weights_and_uids_for_emit", - return_value=(uids_tensor, weights_tensor), + return_value=(uids, weights), ), patch( "bittensor.core.extrinsics.set_weights._do_set_weights", - return_value=(expected_success, "Mock error message"), + return_value=(expected_success, expected_message), ), ): result, message = set_weights_extrinsic( @@ -105,53 +105,10 @@ def test_do_set_weights_is_success(mock_subtensor, fake_wallet, mocker): fake_wait_for_finalization = True mock_subtensor.substrate.submit_extrinsic.return_value.is_success = True - - # Call - result = _do_set_weights( - subtensor=mock_subtensor, - wallet=fake_wallet, - uids=fake_uids, - vals=fake_vals, - netuid=fake_netuid, - version_key=version_as_int, - wait_for_inclusion=fake_wait_for_inclusion, - wait_for_finalization=fake_wait_for_finalization, - ) - - # Asserts - mock_subtensor.substrate.compose_call.assert_called_once_with( - call_module="SubtensorModule", - call_function="set_weights", - call_params={ - "dests": fake_uids, - "weights": fake_vals, - "netuid": fake_netuid, - "version_key": version_as_int, - }, + mocker.patch.object( + mock_subtensor, "sign_and_send_extrinsic", return_value=(True, "") ) - mock_subtensor.substrate.create_signed_extrinsic.assert_called_once() - _, kwargs = mock_subtensor.substrate.create_signed_extrinsic.call_args - assert kwargs["call"] == mock_subtensor.substrate.compose_call.return_value - assert kwargs["keypair"] == fake_wallet.hotkey - assert kwargs["era"] == {"period": 5} - - assert result == (True, "Successfully set weights.") - - -def test_do_set_weights_is_not_success(mock_subtensor, fake_wallet, mocker): - """Unsuccessful _do_set_weights call.""" - # Prep - fake_uids = [1, 2, 3] - fake_vals = [4, 5, 6] - fake_netuid = 1 - fake_wait_for_inclusion = True - fake_wait_for_finalization = True - - mock_subtensor.substrate.submit_extrinsic.return_value.is_success = False - mocked_format_error_message = mocker.MagicMock() - subtensor_module.format_error_message = mocked_format_error_message - # Call result = _do_set_weights( subtensor=mock_subtensor, @@ -176,22 +133,17 @@ def test_do_set_weights_is_not_success(mock_subtensor, fake_wallet, mocker): }, ) - mock_subtensor.substrate.create_signed_extrinsic.assert_called_once() - _, kwargs = mock_subtensor.substrate.create_signed_extrinsic.call_args - assert kwargs["call"] == mock_subtensor.substrate.compose_call.return_value - assert kwargs["keypair"] == fake_wallet.hotkey - assert kwargs["era"] == {"period": 5} - - mock_subtensor.substrate.submit_extrinsic.assert_called_once_with( - extrinsic=mock_subtensor.substrate.create_signed_extrinsic.return_value, + mock_subtensor.sign_and_send_extrinsic.assert_called_once_with( + call=mock_subtensor.substrate.compose_call.return_value, + wallet=fake_wallet, wait_for_inclusion=fake_wait_for_inclusion, wait_for_finalization=fake_wait_for_finalization, + nonce_key="hotkey", + sign_with="hotkey", + use_nonce=True, + period=None, ) - - assert result == ( - False, - "Subtensor returned `UnknownError(UnknownType)` error. This means: `Unknown Description`.", - ) + assert result == (True, "") def test_do_set_weights_no_waits(mock_subtensor, fake_wallet, mocker): @@ -203,6 +155,12 @@ def test_do_set_weights_no_waits(mock_subtensor, fake_wallet, mocker): fake_wait_for_inclusion = False fake_wait_for_finalization = False + mocker.patch.object( + mock_subtensor, + "sign_and_send_extrinsic", + return_value=(True, "Not waiting for finalization or inclusion."), + ) + # Call result = _do_set_weights( subtensor=mock_subtensor, @@ -227,15 +185,11 @@ def test_do_set_weights_no_waits(mock_subtensor, fake_wallet, mocker): }, ) - mock_subtensor.substrate.create_signed_extrinsic.assert_called_once() - _, kwargs = mock_subtensor.substrate.create_signed_extrinsic.call_args - assert kwargs["call"] == mock_subtensor.substrate.compose_call.return_value - assert kwargs["keypair"] == fake_wallet.hotkey - assert kwargs["era"] == {"period": 5} - - mock_subtensor.substrate.submit_extrinsic.assert_called_once_with( - extrinsic=mock_subtensor.substrate.create_signed_extrinsic.return_value, + mock_subtensor.sign_and_send_extrinsic( + call=mock_subtensor.substrate.compose_call.return_value, + wallet=fake_wallet, wait_for_inclusion=fake_wait_for_inclusion, wait_for_finalization=fake_wait_for_finalization, + period=None, ) assert result == (True, "Not waiting for finalization or inclusion.") diff --git a/tests/unit_tests/extrinsics/test_staking.py b/tests/unit_tests/extrinsics/test_staking.py index 899054006f..80ecb5c240 100644 --- a/tests/unit_tests/extrinsics/test_staking.py +++ b/tests/unit_tests/extrinsics/test_staking.py @@ -13,7 +13,7 @@ def test_add_stake_extrinsic(mocker): "sign_and_send_extrinsic.return_value": (True, ""), } ) - fake_wallet = mocker.Mock( + fake_wallet_ = mocker.Mock( **{ "coldkeypub.ss58_address": "hotkey_owner", } @@ -27,7 +27,7 @@ def test_add_stake_extrinsic(mocker): # Call result = staking.add_stake_extrinsic( subtensor=fake_subtensor, - wallet=fake_wallet, + wallet=fake_wallet_, hotkey_ss58=hotkey_ss58, netuid=fake_netuid, amount=amount, @@ -44,13 +44,14 @@ def test_add_stake_extrinsic(mocker): call_params={"hotkey": "hotkey", "amount_staked": 9, "netuid": 1}, ) fake_subtensor.sign_and_send_extrinsic.assert_called_once_with( - fake_subtensor.substrate.compose_call.return_value, - fake_wallet, - True, - True, + call=fake_subtensor.substrate.compose_call.return_value, + wallet=fake_wallet_, + wait_for_inclusion=True, + wait_for_finalization=True, nonce_key="coldkeypub", sign_with="coldkey", use_nonce=True, + period=None, ) @@ -85,7 +86,7 @@ def test_add_stake_multiple_extrinsic(mocker): mocker.patch.object( staking, "get_old_stakes", return_value=[Balance(1.1), Balance(0.3)] ) - fake_wallet = mocker.Mock( + fake_wallet_ = mocker.Mock( **{ "coldkeypub.ss58_address": "hotkey_owner", } @@ -99,7 +100,7 @@ def test_add_stake_multiple_extrinsic(mocker): # Call result = staking.add_stake_multiple_extrinsic( subtensor=fake_subtensor, - wallet=fake_wallet, + wallet=fake_wallet_, hotkey_ss58s=hotkey_ss58s, netuids=netuids, amounts=amounts, @@ -131,11 +132,12 @@ def test_add_stake_multiple_extrinsic(mocker): }, ) fake_subtensor.sign_and_send_extrinsic.assert_called_with( - fake_subtensor.substrate.compose_call.return_value, - fake_wallet, - True, - True, + call=fake_subtensor.substrate.compose_call.return_value, + wallet=fake_wallet_, + wait_for_inclusion=True, + wait_for_finalization=True, nonce_key="coldkeypub", sign_with="coldkey", use_nonce=True, + period=None, ) diff --git a/tests/unit_tests/extrinsics/test_start_call.py b/tests/unit_tests/extrinsics/test_start_call.py index ccc14581ee..ece0e6cf42 100644 --- a/tests/unit_tests/extrinsics/test_start_call.py +++ b/tests/unit_tests/extrinsics/test_start_call.py @@ -3,18 +3,15 @@ def test_start_call_extrinsics(subtensor, mocker, fake_wallet): """Test that start_call_extrinsic correctly constructs and submits the extrinsic.""" - # Preps netuid = 123 wallet = fake_wallet wallet.name = "fake_wallet" wallet.coldkey = "fake_coldkey" - substrate = subtensor.substrate.__enter__.return_value - substrate.compose_call.return_value = "mock_call" - substrate.create_signed_extrinsic.return_value = "signed_ext" - substrate.submit_extrinsic.return_value = mocker.MagicMock( - is_success=True, error_message="" + subtensor.substrate.compose_call = mocker.Mock() + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic", return_value=(True, "") ) # Call @@ -25,21 +22,18 @@ def test_start_call_extrinsics(subtensor, mocker, fake_wallet): ) # Assertions - substrate.compose_call.assert_called_once_with( + subtensor.substrate.compose_call.assert_called_once_with( call_module="SubtensorModule", call_function="start_call", call_params={"netuid": netuid}, ) - substrate.create_signed_extrinsic.assert_called_once_with( - call="mock_call", - keypair=wallet.coldkey, - ) - - substrate.submit_extrinsic.assert_called_once_with( - extrinsic="signed_ext", + mocked_sign_and_send_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, + wallet=wallet, wait_for_inclusion=True, wait_for_finalization=False, + period=None, ) assert success is True diff --git a/tests/unit_tests/extrinsics/test_transfer.py b/tests/unit_tests/extrinsics/test_transfer.py index 9424352a55..fcd532e2d0 100644 --- a/tests/unit_tests/extrinsics/test_transfer.py +++ b/tests/unit_tests/extrinsics/test_transfer.py @@ -10,7 +10,8 @@ def test_do_transfer_is_success_true(subtensor, fake_wallet, mocker): fake_wait_for_inclusion = True fake_wait_for_finalization = True - subtensor.substrate.submit_extrinsic.return_value.is_success = True + mocker.patch.object(subtensor, "sign_and_send_extrinsic", return_value=(True, "")) + mocker.patch.object(subtensor, "get_block_hash", return_value=1) # Call result = _do_transfer( @@ -28,20 +29,15 @@ def test_do_transfer_is_success_true(subtensor, fake_wallet, mocker): call_function="transfer_allow_death", call_params={"dest": fake_dest, "value": fake_transfer_balance.rao}, ) - subtensor.substrate.create_signed_extrinsic.assert_called_once_with( - call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.coldkey - ) - subtensor.substrate.submit_extrinsic.assert_called_once_with( - extrinsic=subtensor.substrate.create_signed_extrinsic.return_value, + subtensor.sign_and_send_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, + wallet=fake_wallet, wait_for_inclusion=fake_wait_for_inclusion, wait_for_finalization=fake_wait_for_finalization, + period=None, ) # subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() - assert result == ( - True, - subtensor.substrate.submit_extrinsic.return_value.block_hash, - "Success with response.", - ) + assert result == (True, 1, "Success with response.") def test_do_transfer_is_success_false(subtensor, fake_wallet, mocker): @@ -52,13 +48,8 @@ def test_do_transfer_is_success_false(subtensor, fake_wallet, mocker): fake_wait_for_inclusion = True fake_wait_for_finalization = True - subtensor.substrate.submit_extrinsic.return_value.is_success = False - - mocked_format_error_message = mocker.Mock() - mocker.patch( - "bittensor.core.extrinsics.transfer.format_error_message", - mocked_format_error_message, - ) + mocker.patch.object(subtensor, "sign_and_send_extrinsic", return_value=(False, "")) + mocker.patch.object(subtensor, "get_block_hash", return_value=1) # Call result = _do_transfer( @@ -76,23 +67,15 @@ def test_do_transfer_is_success_false(subtensor, fake_wallet, mocker): call_function="transfer_allow_death", call_params={"dest": fake_dest, "value": fake_transfer_balance.rao}, ) - subtensor.substrate.create_signed_extrinsic.assert_called_once_with( - call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.coldkey - ) - subtensor.substrate.submit_extrinsic.assert_called_once_with( - extrinsic=subtensor.substrate.create_signed_extrinsic.return_value, + subtensor.sign_and_send_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, + wallet=fake_wallet, wait_for_inclusion=fake_wait_for_inclusion, wait_for_finalization=fake_wait_for_finalization, - ) - mocked_format_error_message.assert_called_once_with( - subtensor.substrate.submit_extrinsic.return_value.error_message + period=None, ) - assert result == ( - False, - "", - mocked_format_error_message.return_value, - ) + assert result == (False, "", "") def test_do_transfer_no_waits(subtensor, fake_wallet, mocker): @@ -103,6 +86,10 @@ def test_do_transfer_no_waits(subtensor, fake_wallet, mocker): fake_wait_for_inclusion = False fake_wait_for_finalization = False + mocker.patch.object( + subtensor, "sign_and_send_extrinsic", return_value=(True, "msg") + ) + # Call result = _do_transfer( subtensor, @@ -119,12 +106,11 @@ def test_do_transfer_no_waits(subtensor, fake_wallet, mocker): call_function="transfer_allow_death", call_params={"dest": fake_dest, "value": fake_transfer_balance.rao}, ) - subtensor.substrate.create_signed_extrinsic.assert_called_once_with( - call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.coldkey - ) - subtensor.substrate.submit_extrinsic.assert_called_once_with( - extrinsic=subtensor.substrate.create_signed_extrinsic.return_value, + subtensor.sign_and_send_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, + wallet=fake_wallet, wait_for_inclusion=fake_wait_for_inclusion, wait_for_finalization=fake_wait_for_finalization, + period=None, ) - assert result == (True, "", "Success, extrinsic submitted without waiting.") + assert result == (True, "", "msg") diff --git a/tests/unit_tests/extrinsics/test_unstaking.py b/tests/unit_tests/extrinsics/test_unstaking.py index 224fe4b640..2fdf0cbe47 100644 --- a/tests/unit_tests/extrinsics/test_unstaking.py +++ b/tests/unit_tests/extrinsics/test_unstaking.py @@ -43,13 +43,14 @@ def test_unstake_extrinsic(fake_wallet, mocker): }, ) fake_subtensor.sign_and_send_extrinsic.assert_called_once_with( - fake_subtensor.substrate.compose_call.return_value, - fake_wallet, - True, - True, + call=fake_subtensor.substrate.compose_call.return_value, + wallet=fake_wallet, + wait_for_inclusion=True, + wait_for_finalization=True, sign_with="coldkey", nonce_key="coldkeypub", use_nonce=True, + period=None, ) @@ -109,11 +110,12 @@ def test_unstake_multiple_extrinsic(fake_wallet, mocker): }, ) fake_subtensor.sign_and_send_extrinsic.assert_called_with( - fake_subtensor.substrate.compose_call.return_value, - fake_wallet, - True, - True, + call=fake_subtensor.substrate.compose_call.return_value, + wallet=fake_wallet, + wait_for_inclusion=True, + wait_for_finalization=True, sign_with="coldkey", nonce_key="coldkeypub", use_nonce=True, + period=None, ) diff --git a/tests/unit_tests/extrinsics/test_utils.py b/tests/unit_tests/extrinsics/test_utils.py index 060f45146b..d77c3cc5e7 100644 --- a/tests/unit_tests/extrinsics/test_utils.py +++ b/tests/unit_tests/extrinsics/test_utils.py @@ -1,26 +1,31 @@ -from unittest.mock import MagicMock - -import pytest -from scalecodec.types import GenericExtrinsic - +from bittensor.core.chain_data import StakeInfo from bittensor.core.extrinsics import utils -from bittensor.core.subtensor import Subtensor - - -@pytest.fixture -def mock_subtensor(mock_substrate): - mock_subtensor = MagicMock(autospec=Subtensor) - mock_subtensor.substrate = mock_substrate - yield mock_subtensor - - -@pytest.fixture -def starting_block(): - yield {"header": {"number": 1, "hash": "0x0100"}} - - -def test_submit_extrinsic_success(mock_subtensor): - mock_subtensor.substrate.submit_extrinsic.return_value = True - mock_extrinsic = MagicMock(autospec=GenericExtrinsic) - result = utils.submit_extrinsic(mock_subtensor, mock_extrinsic, True, True) - assert result is True +from bittensor.utils.balance import Balance + + +def test_old_stake(subtensor, mocker): + wallet = mocker.Mock( + hotkey=mocker.Mock(ss58_address="HK1"), + coldkeypub=mocker.Mock(ss58_address="CK1"), + ) + + expected_stake = Balance.from_tao(100) + + hotkey_ss58s = ["HK1", "HK2"] + netuids = [3, 4] + all_stakes = [ + StakeInfo( + hotkey_ss58="HK1", + coldkey_ss58="CK1", + netuid=3, + stake=expected_stake, + locked=Balance.from_tao(10), + emission=Balance.from_tao(1), + drain=0, + is_registered=True, + ), + ] + + result = utils.get_old_stakes(wallet, hotkey_ss58s, netuids, all_stakes) + + assert result == [expected_stake, Balance.from_tao(0)] diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index ca2a82f603..0449a7b8ef 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -8,10 +8,13 @@ from bittensor import u64_normalized_float from bittensor.core import async_subtensor from bittensor.core.async_subtensor import AsyncSubtensor -from bittensor.core.chain_data.chain_identity import ChainIdentity -from bittensor.core.chain_data.neuron_info import NeuronInfo -from bittensor.core.chain_data.stake_info import StakeInfo -from bittensor.core.chain_data import proposal_vote_data +from bittensor.core.chain_data import ( + proposal_vote_data, + ChainIdentity, + NeuronInfo, + StakeInfo, + SelectiveMetagraphIndex, +) from bittensor.utils import U64_MAX from bittensor.utils.balance import Balance from tests.helpers.helpers import assert_submit_signed_extrinsic @@ -1818,7 +1821,7 @@ async def test_sign_and_send_extrinsic_success_without_inclusion_finalization( wait_for_inclusion=False, wait_for_finalization=False, ) - assert result == (True, "") + assert result == (True, "Not waiting for finalization or inclusion.") @pytest.mark.asyncio @@ -2515,6 +2518,7 @@ async def test_transfer_success(subtensor, fake_wallet, mocker): wait_for_inclusion=True, wait_for_finalization=False, keep_alive=True, + period=None, ) assert result == mocked_transfer_extrinsic.return_value @@ -2548,6 +2552,7 @@ async def test_register_success(subtensor, fake_wallet, mocker): wait_for_finalization=True, wait_for_inclusion=False, wallet=fake_wallet, + period=None, ) assert result == mocked_register_extrinsic.return_value @@ -2663,7 +2668,7 @@ async def test_set_delegate_take_decrease( @pytest.mark.asyncio async def test_set_weights_success(subtensor, fake_wallet, mocker): - """Tests set_weights with successful weight setting on the first try.""" + """Tests set_weights with the successful weight setting on the first try.""" # Preps fake_netuid = 1 fake_uids = [1, 2, 3] @@ -2712,7 +2717,7 @@ async def test_set_weights_success(subtensor, fake_wallet, mocker): wait_for_finalization=False, wait_for_inclusion=False, weights=fake_weights, - period=5, + period=8, ) mocked_weights_rate_limit.assert_called_once_with(fake_netuid) assert result is True @@ -2800,6 +2805,7 @@ async def test_root_set_weights_success(subtensor, fake_wallet, mocker): version_key=0, wait_for_finalization=True, wait_for_inclusion=True, + period=None, ) assert result == mocked_set_root_weights_extrinsic.return_value @@ -2850,6 +2856,7 @@ async def test_commit_weights_success(subtensor, fake_wallet, mocker): commit_hash="fake_commit_hash", wait_for_inclusion=False, wait_for_finalization=False, + period=16, ) assert result is True assert message == "Success" @@ -2952,6 +2959,7 @@ async def test_set_subnet_identity(mocker, subtensor, fake_wallet): additional=fake_subnet_identity.additional, wait_for_finalization=True, wait_for_inclusion=False, + period=None, ) assert result == mocked_extrinsic.return_value @@ -3057,5 +3065,252 @@ async def test_start_call(subtensor, mocker): netuid=netuid, wait_for_inclusion=True, wait_for_finalization=False, + period=None, ) assert result == mocked_extrinsic.return_value + + +# TODO: get back after SelectiveMetagraph come to the mainnet +# @pytest.mark.asyncio +# async def test_get_metagraph_info_all_fields(subtensor, mocker): +# """Test get_metagraph_info with all fields (default behavior).""" +# # Preps +# netuid = 1 +# mock_value = {"mock": "data"} +# +# mock_runtime_call = mocker.patch.object( +# subtensor.substrate, +# "runtime_call", +# return_value=mocker.AsyncMock(value=mock_value), +# ) +# mock_from_dict = mocker.patch.object( +# async_subtensor.MetagraphInfo, "from_dict", return_value="parsed_metagraph" +# ) +# +# # Call +# result = await subtensor.get_metagraph_info(netuid=netuid) +# +# # Asserts +# assert result == "parsed_metagraph" +# mock_runtime_call.assert_awaited_once_with( +# "SubnetInfoRuntimeApi", +# "get_selective_metagraph", +# params=[netuid, SelectiveMetagraphIndex.all_indices()], +# block_hash=await subtensor.determine_block_hash(None), +# ) +# mock_from_dict.assert_called_once_with(mock_value) +# +# +# @pytest.mark.asyncio +# async def test_get_metagraph_info_specific_fields(subtensor, mocker): +# """Test get_metagraph_info with specific fields.""" +# # Preps +# netuid = 1 +# mock_value = {"mock": "data"} +# fields = [SelectiveMetagraphIndex.Name, 5] +# +# mock_runtime_call = mocker.patch.object( +# subtensor.substrate, +# "runtime_call", +# return_value=mocker.AsyncMock(value=mock_value), +# ) +# mock_from_dict = mocker.patch.object( +# async_subtensor.MetagraphInfo, "from_dict", return_value="parsed_metagraph" +# ) +# +# # Call +# result = await subtensor.get_metagraph_info(netuid=netuid, field_indices=fields) +# +# # Asserts +# assert result == "parsed_metagraph" +# mock_runtime_call.assert_awaited_once_with( +# "SubnetInfoRuntimeApi", +# "get_selective_metagraph", +# params=[ +# netuid, +# [0] +# + [ +# f.value if isinstance(f, SelectiveMetagraphIndex) else f for f in fields +# ], +# ], +# block_hash=await subtensor.determine_block_hash(None), +# ) +# mock_from_dict.assert_called_once_with(mock_value) +# +# +# @pytest.mark.parametrize( +# "wrong_fields", +# [ +# [ +# "invalid", +# ], +# [SelectiveMetagraphIndex.Active, 1, "f"], +# [1, 2, 3, "f"], +# ], +# ) +# @pytest.mark.asyncio +# async def test_get_metagraph_info_invalid_field_indices(subtensor, wrong_fields): +# """Test get_metagraph_info raises ValueError on invalid field_indices.""" +# with pytest.raises( +# ValueError, +# match="`field_indices` must be a list of SelectiveMetagraphIndex items.", +# ): +# await subtensor.get_metagraph_info(netuid=1, field_indices=wrong_fields) + + +@pytest.mark.asyncio +async def test_get_metagraph_info_subnet_not_exist(subtensor, mocker): + """Test get_metagraph_info returns None when subnet doesn't exist.""" + netuid = 1 + mocker.patch.object( + subtensor.substrate, + "runtime_call", + return_value=mocker.AsyncMock(value=None), + ) + + mocked_logger = mocker.Mock() + mocker.patch("bittensor.core.subtensor.logging.error", new=mocked_logger) + + result = await subtensor.get_metagraph_info(netuid=netuid) + + assert result is None + mocked_logger.assert_called_once_with(f"Subnet {netuid} does not exist.") + + +@pytest.mark.asyncio +async def test_blocks_since_last_step_with_value(subtensor, mocker): + """Test blocks_since_last_step returns correct value.""" + # preps + netuid = 1 + block = 123 + mocked_query_subtensor = mocker.AsyncMock() + subtensor.query_subtensor = mocked_query_subtensor + + # call + result = await subtensor.blocks_since_last_step(netuid=netuid, block=block) + + # asserts + mocked_query_subtensor.assert_awaited_once_with( + name="BlocksSinceLastStep", + block=block, + block_hash=None, + reuse_block=False, + params=[netuid], + ) + + assert result == mocked_query_subtensor.return_value.value + + +@pytest.mark.asyncio +async def test_blocks_since_last_step_is_none(subtensor, mocker): + """Test blocks_since_last_step returns None correctly.""" + # preps + netuid = 1 + block = 123 + mocked_query_subtensor = mocker.AsyncMock(return_value=None) + subtensor.query_subtensor = mocked_query_subtensor + + # call + result = await subtensor.blocks_since_last_step(netuid=netuid, block=block) + + # asserts + mocked_query_subtensor.assert_awaited_once_with( + name="BlocksSinceLastStep", + block=block, + block_hash=None, + reuse_block=False, + params=[netuid], + ) + + assert result is None + + +@pytest.mark.asyncio +async def test_get_subnet_owner_hotkey_has_return(subtensor, mocker): + """Test get_subnet_owner_hotkey returns correct value.""" + # preps + netuid = 14 + block = 123 + expected_owner_hotkey = "owner_hotkey" + mocked_query_subtensor = mocker.AsyncMock(return_value=expected_owner_hotkey) + subtensor.query_subtensor = mocked_query_subtensor + + # call + result = await subtensor.get_subnet_owner_hotkey(netuid=netuid, block=block) + + # asserts + mocked_query_subtensor.assert_awaited_once_with( + name="SubnetOwnerHotkey", + block=block, + params=[netuid], + ) + + assert result == expected_owner_hotkey + + +@pytest.mark.asyncio +async def test_get_subnet_owner_hotkey_is_none(subtensor, mocker): + """Test get_subnet_owner_hotkey returns None correctly.""" + # preps + netuid = 14 + block = 123 + mocked_query_subtensor = mocker.AsyncMock(return_value=None) + subtensor.query_subtensor = mocked_query_subtensor + + # call + result = await subtensor.get_subnet_owner_hotkey(netuid=netuid, block=block) + + # asserts + mocked_query_subtensor.assert_awaited_once_with( + name="SubnetOwnerHotkey", + block=block, + params=[netuid], + ) + + assert result is None + + +@pytest.mark.asyncio +async def test_get_subnet_validator_permits_has_values(subtensor, mocker): + """Test get_subnet_validator_permits returns correct value.""" + # preps + netuid = 14 + block = 123 + expected_validator_permits = [False, True, False] + mocked_query_subtensor = mocker.AsyncMock(return_value=expected_validator_permits) + subtensor.query_subtensor = mocked_query_subtensor + + # call + result = await subtensor.get_subnet_validator_permits(netuid=netuid, block=block) + + # asserts + mocked_query_subtensor.assert_awaited_once_with( + name="ValidatorPermit", + block=block, + params=[netuid], + ) + + assert result == expected_validator_permits + + +@pytest.mark.asyncio +async def test_get_subnet_validator_permits_is_none(subtensor, mocker): + """Test get_subnet_validator_permits returns correct value.""" + # preps + netuid = 14 + block = 123 + + mocked_query_subtensor = mocker.AsyncMock(return_value=None) + subtensor.query_subtensor = mocked_query_subtensor + + # call + result = await subtensor.get_subnet_validator_permits(netuid=netuid, block=block) + + # asserts + mocked_query_subtensor.assert_awaited_once_with( + name="ValidatorPermit", + block=block, + params=[netuid], + ) + + assert result is None diff --git a/tests/unit_tests/test_dendrite.py b/tests/unit_tests/test_dendrite.py index efbf302b48..38b2f8929f 100644 --- a/tests/unit_tests/test_dendrite.py +++ b/tests/unit_tests/test_dendrite.py @@ -305,7 +305,7 @@ async def test_dendrite__call__success_response( ) mock_aio_response.post( f"http://127.0.0.1:666/SynapseDummy", - body=expected_synapse.json(), + body=expected_synapse.model_dump_json(), ) synapse = await setup_dendrite.call(axon_info, synapse=input_synapse) diff --git a/tests/unit_tests/test_easy_imports.py b/tests/unit_tests/test_easy_imports.py new file mode 100644 index 0000000000..8ebd020faf --- /dev/null +++ b/tests/unit_tests/test_easy_imports.py @@ -0,0 +1,18 @@ +from bittensor.utils import easy_imports +import bittensor + +import pytest + + +@pytest.mark.parametrize( + "attr", + [ + a + for a in dir(easy_imports) + if ( + not a.startswith("__") and a not in ["sys", "importlib"] + ) # we don't care about systemwide pkgs + ], +) +def test_easy_imports(attr): + assert getattr(bittensor, attr), attr diff --git a/tests/unit_tests/test_metagraph.py b/tests/unit_tests/test_metagraph.py index ca8a00ccf6..cdb735f627 100644 --- a/tests/unit_tests/test_metagraph.py +++ b/tests/unit_tests/test_metagraph.py @@ -129,10 +129,10 @@ def mock_subtensor(mocker): @pytest.fixture def metagraph_instance(mocker): metagraph = Metagraph(netuid=1337, sync=False) - metagraph._assign_neurons = mocker.AsyncMock() - metagraph._set_metagraph_attributes = mocker.AsyncMock() - metagraph._set_weights_and_bonds = mocker.AsyncMock() - metagraph._get_all_stakes_from_chain = mocker.AsyncMock() + metagraph._assign_neurons = mocker.Mock() + metagraph._set_metagraph_attributes = mocker.Mock() + metagraph._set_weights_and_bonds = mocker.Mock() + metagraph._get_all_stakes_from_chain = mocker.Mock() return metagraph diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 3bbb8230c3..9085bf1122 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -1,20 +1,3 @@ -# The MIT License (MIT) -# Copyright Š 2024 Opentensor Foundation -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the "Software"), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - import argparse import unittest.mock as mock import datetime @@ -31,7 +14,7 @@ from bittensor.core import subtensor as subtensor_module from bittensor.core.async_subtensor import AsyncSubtensor, logging from bittensor.core.axon import Axon -from bittensor.core.chain_data import SubnetHyperparameters +from bittensor.core.chain_data import SubnetHyperparameters, SelectiveMetagraphIndex from bittensor.core.extrinsics.serving import do_serve_axon from bittensor.core.settings import version_as_int from bittensor.core.subtensor import Subtensor @@ -1220,7 +1203,7 @@ def test_set_weights(subtensor, mocker, fake_wallet): version_key=settings.version_as_int, wait_for_inclusion=fake_wait_for_inclusion, wait_for_finalization=fake_wait_for_finalization, - period=5, + period=8, ) assert result == expected_result @@ -1251,6 +1234,7 @@ def test_serve_axon(subtensor, mocker): wait_for_inclusion=fake_wait_for_inclusion, wait_for_finalization=fake_wait_for_finalization, certificate=fake_certificate, + period=None, ) assert result == mocked_serve_axon_extrinsic.return_value @@ -1269,7 +1253,7 @@ def test_get_block_hash(subtensor, mocker): def test_commit(subtensor, fake_wallet, mocker): - """Test successful commit call.""" + """Test a successful commit call.""" # Preps fake_netuid = 1 fake_data = "some data to network" @@ -1285,6 +1269,7 @@ def test_commit(subtensor, fake_wallet, mocker): netuid=fake_netuid, data_type=f"Raw{len(fake_data)}", data=fake_data.encode(), + period=None, ) assert result is mocked_publish_metadata.return_value @@ -1342,6 +1327,7 @@ def test_transfer(subtensor, fake_wallet, mocker): wait_for_inclusion=fake_wait_for_inclusion, wait_for_finalization=fake_wait_for_finalization, keep_alive=True, + period=None, ) assert result == mocked_transfer_extrinsic.return_value @@ -1483,7 +1469,7 @@ def test_do_serve_axon_is_success( fake_wait_for_inclusion = True fake_wait_for_finalization = True - subtensor.substrate.submit_extrinsic.return_value.is_success = True + mocker.patch.object(subtensor, "sign_and_send_extrinsic", return_value=(True, "")) # Call result = do_serve_axon( @@ -1501,20 +1487,16 @@ def test_do_serve_axon_is_success( call_params=fake_call_params, ) - subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + subtensor.sign_and_send_extrinsic.assert_called_once_with( call=subtensor.substrate.compose_call.return_value, - keypair=fake_wallet.hotkey, - ) - - subtensor.substrate.submit_extrinsic.assert_called_once_with( - extrinsic=subtensor.substrate.create_signed_extrinsic.return_value, + wallet=fake_wallet, wait_for_inclusion=fake_wait_for_inclusion, wait_for_finalization=fake_wait_for_finalization, + period=None, ) - # subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() assert result[0] is True - assert result[1] is None + assert result[1] is "" def test_do_serve_axon_is_not_success(subtensor, fake_wallet, mocker, fake_call_params): @@ -1523,7 +1505,9 @@ def test_do_serve_axon_is_not_success(subtensor, fake_wallet, mocker, fake_call_ fake_wait_for_inclusion = True fake_wait_for_finalization = True - subtensor.substrate.submit_extrinsic.return_value.is_success = None + mocker.patch.object( + subtensor, "sign_and_send_extrinsic", return_value=(False, None) + ) # Call result = do_serve_axon( @@ -1541,21 +1525,15 @@ def test_do_serve_axon_is_not_success(subtensor, fake_wallet, mocker, fake_call_ call_params=fake_call_params, ) - subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + subtensor.sign_and_send_extrinsic.assert_called_once_with( call=subtensor.substrate.compose_call.return_value, - keypair=fake_wallet.hotkey, - ) - - subtensor.substrate.submit_extrinsic.assert_called_once_with( - extrinsic=subtensor.substrate.create_signed_extrinsic.return_value, + wallet=fake_wallet, wait_for_inclusion=fake_wait_for_inclusion, wait_for_finalization=fake_wait_for_finalization, + period=None, ) - assert result == ( - False, - subtensor.substrate.submit_extrinsic.return_value.error_message, - ) + assert result == (False, None) def test_do_serve_axon_no_waits(subtensor, fake_wallet, mocker, fake_call_params): @@ -1564,6 +1542,11 @@ def test_do_serve_axon_no_waits(subtensor, fake_wallet, mocker, fake_call_params fake_wait_for_inclusion = False fake_wait_for_finalization = False + mocked_sign_and_send_extrinsic = mocker.Mock(return_value=(True, "")) + mocker.patch.object( + subtensor, "sign_and_send_extrinsic", new=mocked_sign_and_send_extrinsic + ) + # Call result = do_serve_axon( subtensor=subtensor, @@ -1580,17 +1563,14 @@ def test_do_serve_axon_no_waits(subtensor, fake_wallet, mocker, fake_call_params call_params=fake_call_params, ) - subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + mocked_sign_and_send_extrinsic.assert_called_once_with( call=subtensor.substrate.compose_call.return_value, - keypair=fake_wallet.hotkey, - ) - - subtensor.substrate.submit_extrinsic.assert_called_once_with( - extrinsic=subtensor.substrate.create_signed_extrinsic.return_value, + wallet=fake_wallet, wait_for_inclusion=fake_wait_for_inclusion, wait_for_finalization=fake_wait_for_finalization, + period=None, ) - assert result == (True, None) + assert result == (True, "") def test_immunity_period(subtensor, mocker): @@ -1980,6 +1960,7 @@ def test_commit_weights(subtensor, fake_wallet, mocker): commit_hash=mocked_generate_weight_hash.return_value, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=16, ) assert result == expected_result @@ -2019,6 +2000,7 @@ def test_reveal_weights(subtensor, fake_wallet, mocker): salt=salt, wait_for_inclusion=False, wait_for_finalization=False, + period=16, ) @@ -2276,7 +2258,6 @@ def test_networks_during_connection(mock_substrate, mocker): sub.chain_endpoint = settings.NETWORK_MAP.get(network) -@pytest.mark.asyncio def test_get_stake_for_coldkey_and_hotkey(subtensor, mocker): netuids = [1, 2, 3] stake_info_dict = { @@ -2849,6 +2830,7 @@ def test_add_stake_success(mocker, fake_wallet, subtensor): safe_staking=False, allow_partial_stake=False, rate_tolerance=0.005, + period=None, ) assert result == mock_add_stake_extrinsic.return_value @@ -2888,6 +2870,7 @@ def test_add_stake_with_safe_staking(mocker, fake_wallet, subtensor): safe_staking=True, allow_partial_stake=False, rate_tolerance=fake_rate_tolerance, + period=None, ) assert result == mock_add_stake_extrinsic.return_value @@ -2921,6 +2904,7 @@ def test_add_stake_multiple_success(mocker, fake_wallet, subtensor): amounts=fake_amount, wait_for_inclusion=True, wait_for_finalization=False, + period=None, ) assert result == mock_add_stake_multiple_extrinsic.return_value @@ -2957,6 +2941,7 @@ def test_unstake_success(mocker, subtensor, fake_wallet): safe_staking=False, allow_partial_stake=False, rate_tolerance=0.005, + period=None, ) assert result == mock_unstake_extrinsic.return_value @@ -2993,6 +2978,7 @@ def test_unstake_with_safe_staking(mocker, subtensor, fake_wallet): safe_staking=True, allow_partial_stake=True, rate_tolerance=fake_rate_tolerance, + period=None, ) assert result == mock_unstake_extrinsic.return_value @@ -3036,6 +3022,7 @@ def test_swap_stake_success(mocker, subtensor, fake_wallet): safe_staking=False, allow_partial_stake=False, rate_tolerance=0.005, + period=None, ) assert result == mock_swap_stake_extrinsic.return_value @@ -3080,6 +3067,7 @@ def test_swap_stake_with_safe_staking(mocker, subtensor, fake_wallet): safe_staking=True, allow_partial_stake=True, rate_tolerance=fake_rate_tolerance, + period=None, ) assert result == mock_swap_stake_extrinsic.return_value @@ -3113,6 +3101,7 @@ def test_unstake_multiple_success(mocker, subtensor, fake_wallet): amounts=fake_amounts, wait_for_inclusion=True, wait_for_finalization=False, + period=None, ) assert result == mock_unstake_multiple_extrinsic.return_value @@ -3161,6 +3150,7 @@ def test_set_weights_with_commit_reveal_enabled(subtensor, fake_wallet, mocker): wait_for_inclusion=fake_wait_for_inclusion, wait_for_finalization=fake_wait_for_finalization, block_time=12.0, + period=8, ) assert result == mocked_commit_reveal_v3_extrinsic.return_value @@ -3218,6 +3208,7 @@ def test_set_subnet_identity(mocker, subtensor, fake_wallet): additional=fake_subnet_identity.additional, wait_for_finalization=True, wait_for_inclusion=False, + period=None, ) assert result == mocked_extrinsic.return_value @@ -3407,5 +3398,238 @@ def test_start_call(subtensor, mocker): netuid=netuid, wait_for_inclusion=True, wait_for_finalization=False, + period=None, ) assert result == mocked_extrinsic.return_value + + +# TODO: get back after SelectiveMetagraph come to the mainnet +# def test_get_metagraph_info_all_fields(subtensor, mocker): +# """Test get_metagraph_info with all fields (default behavior).""" +# # Preps +# netuid = 1 +# mock_value = {"mock": "data"} +# +# mock_runtime_call = mocker.patch.object( +# subtensor.substrate, +# "runtime_call", +# return_value=mocker.Mock(value=mock_value), +# ) +# mock_from_dict = mocker.patch.object( +# subtensor_module.MetagraphInfo, "from_dict", return_value="parsed_metagraph" +# ) +# +# # Call +# result = subtensor.get_metagraph_info(netuid=netuid) +# +# # Asserts +# assert result == "parsed_metagraph" +# mock_runtime_call.assert_called_once_with( +# "SubnetInfoRuntimeApi", +# "get_selective_metagraph", +# params=[netuid, SelectiveMetagraphIndex.all_indices()], +# block_hash=subtensor.determine_block_hash(None), +# ) +# mock_from_dict.assert_called_once_with(mock_value) +# +# +# def test_get_metagraph_info_specific_fields(subtensor, mocker): +# """Test get_metagraph_info with specific fields.""" +# # Preps +# netuid = 1 +# mock_value = {"mock": "data"} +# fields = [SelectiveMetagraphIndex.Name, 5] +# +# mock_runtime_call = mocker.patch.object( +# subtensor.substrate, +# "runtime_call", +# return_value=mocker.Mock(value=mock_value), +# ) +# mock_from_dict = mocker.patch.object( +# subtensor_module.MetagraphInfo, "from_dict", return_value="parsed_metagraph" +# ) +# +# # Call +# result = subtensor.get_metagraph_info(netuid=netuid, field_indices=fields) +# +# # Asserts +# assert result == "parsed_metagraph" +# mock_runtime_call.assert_called_once_with( +# "SubnetInfoRuntimeApi", +# "get_selective_metagraph", +# params=[ +# netuid, +# [0] +# + [ +# f.value if isinstance(f, SelectiveMetagraphIndex) else f for f in fields +# ], +# ], +# block_hash=subtensor.determine_block_hash(None), +# ) +# mock_from_dict.assert_called_once_with(mock_value) +# +# +# @pytest.mark.parametrize( +# "wrong_fields", +# [ +# [ +# "invalid", +# ], +# [SelectiveMetagraphIndex.Active, 1, "f"], +# [1, 2, 3, "f"], +# ], +# ) +# def test_get_metagraph_info_invalid_field_indices(subtensor, wrong_fields): +# """Test get_metagraph_info raises ValueError on invalid field_indices.""" +# with pytest.raises( +# ValueError, +# match="`field_indices` must be a list of SelectiveMetagraphIndex items.", +# ): +# subtensor.get_metagraph_info(netuid=1, field_indices=wrong_fields) + + +def test_get_metagraph_info_subnet_not_exist(subtensor, mocker): + """Test get_metagraph_info returns None when subnet doesn't exist.""" + netuid = 1 + mocker.patch.object( + subtensor.substrate, + "runtime_call", + return_value=mocker.Mock(value=None), + ) + + mocked_logger = mocker.Mock() + mocker.patch("bittensor.core.subtensor.logging.error", new=mocked_logger) + + result = subtensor.get_metagraph_info(netuid=netuid) + + assert result is None + mocked_logger.assert_called_once_with(f"Subnet {netuid} does not exist.") + + +def test_blocks_since_last_step_with_value(subtensor, mocker): + """Test blocks_since_last_step returns correct value.""" + # preps + netuid = 1 + block = 123 + mocked_query_subtensor = mocker.MagicMock() + subtensor.query_subtensor = mocked_query_subtensor + + # call + result = subtensor.blocks_since_last_step(netuid=netuid, block=block) + + # asserts + mocked_query_subtensor.assert_called_once_with( + name="BlocksSinceLastStep", + block=block, + params=[netuid], + ) + + assert result == mocked_query_subtensor.return_value.value + + +def test_blocks_since_last_step_is_none(subtensor, mocker): + """Test blocks_since_last_step returns None correctly.""" + # preps + netuid = 1 + block = 123 + mocked_query_subtensor = mocker.MagicMock(return_value=None) + subtensor.query_subtensor = mocked_query_subtensor + + # call + result = subtensor.blocks_since_last_step(netuid=netuid, block=block) + + # asserts + mocked_query_subtensor.assert_called_once_with( + name="BlocksSinceLastStep", + block=block, + params=[netuid], + ) + + assert result is None + + +def test_get_subnet_owner_hotkey_has_return(subtensor, mocker): + """Test get_subnet_owner_hotkey returns correct value.""" + # preps + netuid = 14 + block = 123 + expected_owner_hotkey = "owner_hotkey" + mocked_query_subtensor = mocker.MagicMock(return_value=expected_owner_hotkey) + subtensor.query_subtensor = mocked_query_subtensor + + # call + result = subtensor.get_subnet_owner_hotkey(netuid=netuid, block=block) + + # asserts + mocked_query_subtensor.assert_called_once_with( + name="SubnetOwnerHotkey", + block=block, + params=[netuid], + ) + + assert result == expected_owner_hotkey + + +def test_get_subnet_owner_hotkey_is_none(subtensor, mocker): + """Test get_subnet_owner_hotkey returns None correctly.""" + # preps + netuid = 14 + block = 123 + mocked_query_subtensor = mocker.MagicMock(return_value=None) + subtensor.query_subtensor = mocked_query_subtensor + + # call + result = subtensor.get_subnet_owner_hotkey(netuid=netuid, block=block) + + # asserts + mocked_query_subtensor.assert_called_once_with( + name="SubnetOwnerHotkey", + block=block, + params=[netuid], + ) + + assert result is None + + +def test_get_subnet_validator_permits_has_values(subtensor, mocker): + """Test get_subnet_validator_permits returns correct value.""" + # preps + netuid = 14 + block = 123 + expected_validator_permits = [False, True, False] + mocked_query_subtensor = mocker.MagicMock(return_value=expected_validator_permits) + subtensor.query_subtensor = mocked_query_subtensor + + # call + result = subtensor.get_subnet_validator_permits(netuid=netuid, block=block) + + # asserts + mocked_query_subtensor.assert_called_once_with( + name="ValidatorPermit", + block=block, + params=[netuid], + ) + + assert result == expected_validator_permits + + +def test_get_subnet_validator_permits_is_none(subtensor, mocker): + """Test get_subnet_validator_permits returns correct value.""" + # preps + netuid = 14 + block = 123 + + mocked_query_subtensor = mocker.MagicMock(return_value=None) + subtensor.query_subtensor = mocked_query_subtensor + + # call + result = subtensor.get_subnet_validator_permits(netuid=netuid, block=block) + + # asserts + mocked_query_subtensor.assert_called_once_with( + name="ValidatorPermit", + block=block, + params=[netuid], + ) + + assert result is None diff --git a/tests/unit_tests/test_subtensor_api.py b/tests/unit_tests/test_subtensor_api.py new file mode 100644 index 0000000000..35d6c6a15c --- /dev/null +++ b/tests/unit_tests/test_subtensor_api.py @@ -0,0 +1,94 @@ +from bittensor.core.subtensor import Subtensor +from bittensor.core.subtensor_api import SubtensorApi +import pytest + + +def test_properties_methods_comparable(other_class: "Subtensor" = None): + """Verifies that methods in SubtensorApi and its properties contains all Subtensors methods.""" + # Preps + subtensor = other_class(_mock=True) if other_class else Subtensor(_mock=True) + subtensor_api = SubtensorApi(mock=True) + + subtensor_methods = [m for m in dir(subtensor) if not m.startswith("_")] + + excluded_subtensor_methods = ["commit"] + + subtensor_api_methods = [m for m in dir(subtensor_api) if not m.startswith("_")] + chain_methods = [m for m in dir(subtensor_api.chain) if not m.startswith("_")] + commitments_methods = [ + m for m in dir(subtensor_api.commitments) if not m.startswith("_") + ] + delegates_methods = [ + m for m in dir(subtensor_api.delegates) if not m.startswith("_") + ] + extrinsics_methods = [ + m for m in dir(subtensor_api.extrinsics) if not m.startswith("_") + ] + metagraphs_methods = [ + m for m in dir(subtensor_api.metagraphs) if not m.startswith("_") + ] + neurons_methods = [m for m in dir(subtensor_api.neurons) if not m.startswith("_")] + queries_methods = [m for m in dir(subtensor_api.queries) if not m.startswith("_")] + stakes_methods = [m for m in dir(subtensor_api.staking) if not m.startswith("_")] + subnets_methods = [m for m in dir(subtensor_api.subnets) if not m.startswith("_")] + wallets_methods = [m for m in dir(subtensor_api.wallets) if not m.startswith("_")] + + all_subtensor_api_methods = ( + subtensor_api_methods + + chain_methods + + commitments_methods + + delegates_methods + + extrinsics_methods + + metagraphs_methods + + neurons_methods + + queries_methods + + stakes_methods + + subnets_methods + + wallets_methods + ) + + # Assertions + for method in subtensor_methods: + # skipp excluded methods + if method in excluded_subtensor_methods: + continue + assert method in all_subtensor_api_methods, ( + f"`Subtensor.{method}`is not present in class `SubtensorApi`." + ) + + +def test__methods_comparable_with_passed_legacy_methods( + other_class: "Subtensor" = None, +): + """Verifies that methods in SubtensorApi contains all Subtensors methods if `legacy_methods=True` is passed.""" + # Preps + subtensor = other_class(mock=True) if other_class else Subtensor(_mock=True) + subtensor_api = SubtensorApi(mock=True, legacy_methods=True) + + subtensor_methods = [m for m in dir(subtensor) if not m.startswith("_")] + subtensor_api_methods = [m for m in dir(subtensor_api) if not m.startswith("_")] + + excluded_subtensor_methods = ["commit"] + + # Assertions + for method in subtensor_methods: + # skipp excluded methods + if method in excluded_subtensor_methods: + continue + assert method in subtensor_api_methods, ( + f"`Subtensor.{method}`is not present in class `SubtensorApi`." + ) + + +def test_failed_if_subtensor_has_new_method(): + """Verifies that SubtensorApi fails if Subtensor has a new method.""" + # Preps + + class SubtensorWithNewMethod(Subtensor): + def return_my_id(self): + return id(self) + + # Call and assert + + with pytest.raises(AssertionError): + test_properties_methods_comparable(other_class=SubtensorWithNewMethod) diff --git a/tests/unit_tests/test_subtensor_extended.py b/tests/unit_tests/test_subtensor_extended.py index ec67747f97..45f65f24db 100644 --- a/tests/unit_tests/test_subtensor_extended.py +++ b/tests/unit_tests/test_subtensor_extended.py @@ -1291,59 +1291,6 @@ def test_root_register_is_already_registered( mock_substrate.submit_extrinsic.assert_not_called() -def test_root_set_weights(mock_substrate, subtensor, fake_wallet, mocker): - MIN_ALLOWED_WEIGHTS = 0 - MAX_WEIGHTS_LIMIT = 1 - - mock_substrate.query.return_value = 1 - mocker.patch.object( - subtensor, - "get_hyperparameter", - autospec=True, - side_effect=[ - MIN_ALLOWED_WEIGHTS, - MAX_WEIGHTS_LIMIT, - ], - ) - - subtensor.root_set_weights( - fake_wallet, - netuids=[1, 2], - weights=[0.5, 0.5], - ) - - subtensor.get_hyperparameter.assert_has_calls( - [ - mocker.call("MinAllowedWeights", netuid=0), - mocker.call("MaxWeightsLimit", netuid=0), - ] - ) - mock_substrate.query.assert_called_once_with( - "SubtensorModule", - "Uids", - [0, fake_wallet.hotkey.ss58_address], - ) - - assert_submit_signed_extrinsic( - mock_substrate, - fake_wallet.coldkey, - call_module="SubtensorModule", - call_function="set_root_weights", - call_params={ - "dests": [1, 2], - "weights": [65535, 65535], - "netuid": 0, - "version_key": 0, - "hotkey": fake_wallet.hotkey.ss58_address, - }, - era={ - "period": 5, - }, - nonce=mock_substrate.get_account_next_index.return_value, - wait_for_finalization=False, - ) - - def test_root_set_weights_no_uid(mock_substrate, subtensor, fake_wallet, mocker): mock_substrate.query.return_value = None